﻿<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>Ayende @ Rahien</title><link>https://ayende.com/blog/</link><description>Ayende @ Rahien</description><copyright>Copyright (C) Ayende Rahien  2004 - 2021 (c) 2026</copyright><ttl>60</ttl><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>https://ayende.com/blog/203843-C/the-hole-in-my-falloaction?Key=792324ea-5ab1-498a-b596-3082054f0601</link><guid>https://ayende.com/blog/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>https://ayende.com/blog/203811-A/a-tale-of-one-off-coding-agents-and-the-shortest-path-to-victory?Key=7d74c708-dfba-44a4-9fea-d70a6e35c936</link><guid>https://ayende.com/blog/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>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>https://ayende.com/blog/203747-A/api-design-dont-try-to-guess?Key=683e3e2e-0a5f-4f10-a978-04e0f2fab851</link><guid>https://ayende.com/blog/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>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>https://ayende.com/blog/203651-B/swiping-left-on-writing-authentication-code-yourself?Key=80bc09c8-a4ec-4a64-9479-58e7f514ee6e</link><guid>https://ayende.com/blog/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>https://ayende.com/blog/203622-B/propertysphere-bot-understanding-images?Key=889433c1-a0cb-4633-b096-390e184c9159</link><guid>https://ayende.com/blog/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>https://ayende.com/blog/203621-B/propertyspheres-intelligent-telegram-bot?Key=1802d1b2-62ad-4016-9d1f-cac2ebf1acff</link><guid>https://ayende.com/blog/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>https://ayende.com/blog/203620-B/propertysphere-sample-application?Key=2ab9814a-35aa-48ee-86f3-f3f9b1accd57</link><guid>https://ayende.com/blog/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>https://ayende.com/blog/203587-B/ravendb-kubernetes-operator?Key=1d5af4d7-9a6e-42a5-84a2-5d446342e2bb</link><guid>https://ayende.com/blog/203587-B/ravendb-kubernetes-operator?Key=1d5af4d7-9a6e-42a5-84a2-5d446342e2bb</guid><pubDate>Mon, 15 Dec 2025 12:00:00 GMT</pubDate></item><item><title>Implementing Agentic Reminders in RavenDB</title><description>&lt;p&gt;A really interesting problem for developers building agentic systems is moving away from chatting with the AI model. For example, consider the following conversation:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/kigqhoDT1Wih1A8CpRXj9A.png"/&gt;&lt;/p&gt;&lt;p&gt;This is a pretty simple scenario where we need to actually step out of the chat and do something else. This seems like an obvious request, right? But it turns out to be a bit complex to build.&lt;/p&gt;&lt;p&gt;The reason for that is simple. AI models don&amp;rsquo;t actually behave like you would expect them to if your usage is primarily as a chat interface. Here is a typical invocation of a model in code:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-csharp'&gt;&lt;code class='line-numbers language-csharp'&gt;&lt;span class="token keyword"&gt;class&lt;/span&gt; &lt;span class="token class-name"&gt;MessageTuple&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;NamedTuple&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    role&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token class-name"&gt;str&lt;/span&gt;
    content&lt;span class="token punctuation"&gt;:&lt;/span&gt; str


&lt;span class="token return-type class-name"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;call_model&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token named-parameter punctuation"&gt;message_history&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; List&lt;span class="token punctuation"&gt;[&lt;/span&gt;MessageTuple&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token named-parameter punctuation"&gt;tools&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; List&lt;span class="token punctuation"&gt;[&lt;/span&gt;Callable&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; None
&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
   pass # redacted&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;In other words, it is the responsibility of the caller to keep track of the conversation and send the &lt;em&gt;entire&lt;/em&gt;&amp;nbsp;conversation to the agent on each round. Here is what this looks like in code:&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;conversation_history = &lt;span class="token punctuation"&gt;[&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token property"&gt;"role"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"user"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token property"&gt;"content"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"When do I get my anniversary gift?"&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 property"&gt;"role"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"agent"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token property"&gt;"content"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"Based on our records, your two-year anniversary is in three days. This milestone means you're eligible for a gift card as part of our company's recognition program.\nOur policy awards a $100 gift card for each year of service. Since you've completed two years, a $200 gift card will be sent to you via SMS on October 1, 2025."&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 property"&gt;"role"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"user"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token property"&gt;"content"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"Remind me to double check I got that in a week"&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;Let&amp;rsquo;s assume that we have a tool call for setting up reminders for users. In RavenDB, this looks like the screenshot below (&lt;a href="https://docs.ravendb.net/7.1/ai-integration/ai-agents/creating-ai-agents/creating-ai-agents_api#action-tools"&gt;more on agentic actions in RavenDB here&lt;/a&gt;):&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/uVtTZ17MGY_Vx9AVw05Y_Q.png"/&gt;&lt;/p&gt;&lt;p&gt;And in the backend, we have the following code:&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;CreateReminderArgs&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;"CreateReminder"&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; &lt;span class="token punctuation"&gt;(&lt;/span&gt;args&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 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; at &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime.Parse&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;args&lt;span class="token punctuation"&gt;.&lt;/span&gt;at&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; reminder &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;Reminder&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;EmployeeId&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;request&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;EmployeeId&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;&lt;span class="token namespace"&gt;conversation&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;Message&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; args&lt;span class="token punctuation"&gt;.&lt;/span&gt;msg&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;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;reminder&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;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Advanced.GetMetadataFor&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;reminder&lt;span class="token punctuation"&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;"@refresh"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; at&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;"Reminder set for {at} {reminder.Id}"&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;This code uses several of RavenDB&amp;rsquo;s features to perform its task. First we have the conversation handler, which is the backend handling for the tool call we just saw. Next we have the use of the &lt;code&gt;@refresh &lt;/code&gt;feature of RavenDB. &lt;a href="https://ayende.com/blog/203203-B/scheduling-with-ravendb"&gt;I recently posted about how you can use this feature for scheduling&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;In short, we set up a &lt;a href="https://docs.ravendb.net/6.2/client-api/data-subscriptions/what-are-data-subscriptions/"&gt;RavenDB Subscription&lt;/a&gt;&amp;nbsp;Task to be called when those reminders should be raised. Here is what the subscription looks like:&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&amp;lt;pre class='line-numbers language-javascript'&amp;gt;&amp;lt;code class='line-numbers language-javascript'&amp;gt;from Reminders &lt;span class="token keyword"&gt;as&lt;/span&gt; r
where r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token string"&gt;'@metadata'&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token string"&gt;'@refresh'&lt;/span&gt; &lt;span class="token operator"&gt;!=&lt;/span&gt; &lt;span class="token keyword"&gt;null&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;p&gt;And here is the client code to actually handle it:&lt;/p&gt;&lt;hr/&gt;&amp;lt;pre class='line-numbers language-javascript'&amp;gt;&amp;lt;code class='line-numbers language-javascript'&amp;gt;&lt;span class="token keyword"&gt;async&lt;/span&gt; Task &lt;span class="token function"&gt;HandleReminder&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;Reminder reminder&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; conversation &lt;span class="token operator"&gt;=&lt;/span&gt; _documentStore&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;AI&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Conversation&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
                &lt;span class="token literal-property property"&gt;agentId&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;&amp;quot;smartest-agent&amp;quot;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                reminder&lt;span class="token punctuation"&gt;.&lt;/span&gt;ConversationId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token literal-property property"&gt;creationOptions&lt;/span&gt;&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;
     conversation&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;AddArtificialActionWithResponse&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
&lt;span class="token string"&gt;&amp;quot;GetRaisedReminders&amp;quot;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; reminder&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; result &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; conversation&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;RunAsync&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;await&lt;/span&gt; &lt;span class="token function"&gt;MessageUser&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;conversation&lt;span class="token punctuation"&gt;,&lt;/span&gt; result&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;The question now is, what should we &lt;em&gt;do&lt;/em&gt;&amp;nbsp;with the reminder?&lt;/p&gt;&lt;p&gt;Going back to the top of this post, we know that we need to add the reminder to the conversation. The problem is that this isn&amp;rsquo;t part of the actual model of the conversation. This is neither a user prompt nor a model answer. How do we deal with this?&lt;/p&gt;&lt;p&gt;We use a really elegant approach here: we inject an artificial tool call into the conversation history. This makes the model think that it checked for reminders and received one in return, even though this happened outside the chat. This lets the agent respond naturally, as if the reminder were part of the ongoing conversation, preserving the full context.&lt;/p&gt;&lt;p&gt;Finally, since we&amp;rsquo;re not actively chatting with the user at this point, we need to send a message prompting them to check back on the conversation with the model.&lt;/p&gt;&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;This is a high-level post, meant specifically to give you some ideas about how you can take your agentic systems to a higher level than a simple chat with the model. The reminder example is a pretty straightforward example, but a truly powerful one. It transforms a simple chat into a much more complex interaction model with the AI.&lt;/p&gt;&lt;p&gt;RavenDB&amp;rsquo;s unique approach of &amp;quot;inserting&amp;quot; a tool call back into the conversation history effectively tells the AI model, &amp;quot;I&amp;#39;ve checked for reminders and found a reminder for this user.&amp;quot; This allows the agent to handle the reminder within the context of the original conversation, rather than initiating a new one. It also allows the agent to maintain a single, coherent conversational thread with the user, even when the system needs to perform background tasks and re-engage with them later.&lt;/p&gt;&lt;p&gt;You can also use the same infrastructure to create a &lt;em&gt;new&lt;/em&gt;&amp;nbsp;conversation, if that makes sense in your domain, and use the previous conversation as &amp;ldquo;background material&amp;rdquo;, so to speak. There is a wide variety of options available to fit your exact scenario.&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;&lt;/p&gt;
</description><link>https://ayende.com/blog/203523-C/implementing-agentic-reminders-in-ravendb?Key=a4ed6127-e4fa-4df6-b2a5-4f17b1792a3b</link><guid>https://ayende.com/blog/203523-C/implementing-agentic-reminders-in-ravendb?Key=a4ed6127-e4fa-4df6-b2a5-4f17b1792a3b</guid><pubDate>Mon, 08 Dec 2025 12:00:00 GMT</pubDate></item><item><title>Recording: From CRUD TO AI – building an intelligent Telegram bot in &lt; 200 lines of code with RavenDB</title><description>&lt;p&gt;Want to see how modern applications handle complexity, scale, and cutting-edge features without becoming unmanageable? In this deep-dive webinar, we move From CRUD to AI Agents, showcasing how RavenDB, a high-performance document database, simplifies the development of a complex Property Management application.&lt;/p&gt;&lt;p&gt;&lt;iframe width="1840" height="1035" title="From CRUD to AI Agents How To Build a Telegram Bot with Vector Search in 200 Lines of Code | RavenDB" src="https://www.youtube.com/embed/XOdXDNIGzxE" frameborder="0" allowfullscreen="" referrerpolicy="strict-origin-when-cross-origin" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"&gt;&lt;/iframe&gt;&lt;/p&gt;</description><link>https://ayende.com/blog/203459-B/recording-from-crud-to-ai-building-an-intelligent-telegram-bot-in-200-lines-of-code-with-ravendb?Key=61bbfd87-8de3-48e7-81a3-6f7fc69d3eb9</link><guid>https://ayende.com/blog/203459-B/recording-from-crud-to-ai-building-an-intelligent-telegram-bot-in-200-lines-of-code-with-ravendb?Key=61bbfd87-8de3-48e7-81a3-6f7fc69d3eb9</guid><pubDate>Tue, 02 Dec 2025 12:00:00 GMT</pubDate></item><item><title>Using multi-staged actions with AI Agents to reduce costs &amp; time</title><description>&lt;p&gt;When building AI Agents, one of the challenges you have to deal with is the sheer amount of data that the agent may need to go through. A natural way to deal with that is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;to hand the information directly to the model, but rather allow it to query for the information as it sees fit.&lt;/p&gt;&lt;p&gt;For example, in the case of a human resource assistant, we may want to expose the employer&amp;rsquo;s policies to the agent, so it can answer questions such as &amp;ldquo;What is the required holiday request time?&amp;rdquo;.&lt;/p&gt;&lt;p&gt;We can do that easily enough using the following agent-query mechanism:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/cCCT52Mq4ifxZuCvxXcHfg.png"/&gt;&lt;/p&gt;&lt;p&gt;If the agent needs to answer a question about a policy, it can use this tool to get the policies and find out what the answer is.&lt;/p&gt;&lt;p&gt;That works if you are a mom &amp;amp; pop shop, but what happens if you happen to be a big organization, with policies on everything from requesting time off to bringing your own device to modern slavery prohibition? Calling this tool is going to give &lt;em&gt;all &lt;/em&gt;those policies to the model? &lt;/p&gt;&lt;p&gt;That is going to be &lt;em&gt;incredibly&lt;/em&gt;&amp;nbsp;expensive, since you have to burn through a lot of tokens that are simply not relevant to the problem at hand.&lt;/p&gt;&lt;p&gt;The next step is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;to return all of the policies and filter them. We can do that using vector search and utilize the model&amp;rsquo;s understanding of the data to help us find exactly what we want.&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/bmTBXHayEHFLnzmm1E8qmA.png"/&gt;&lt;/p&gt;&lt;p&gt;That is much better, but a search for &amp;ldquo;confidentiality contract&amp;rdquo; will get you the Non-Disclosure Agreement as well as the processes for hiring a new employee when their current employer isn&amp;rsquo;t aware they are looking, etc.&lt;/p&gt;&lt;p&gt;That can &lt;em&gt;still&lt;/em&gt;&amp;nbsp;be a lot of text to go through. It isn&amp;rsquo;t as much as &lt;em&gt;everything&lt;/em&gt;, but still a pretty heavy weight. &lt;/p&gt;&lt;p&gt;A nice alternative to this is to break it into two separate operations, as you can see below:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/d4G2hpZReWWKxjY-hGu05g.png"/&gt;&lt;/p&gt;&lt;p&gt;The model will first run the &lt;code&gt;FindPolicies&lt;/code&gt;&amp;nbsp;query to get the list of potential policies. It can then decide, based on their titles, which ones it is actually interested in reading the full text of. &lt;/p&gt;&lt;p&gt;You need to perform two tool calls in this case, but it actually ends up being both faster and cheaper in the end. &lt;/p&gt;&lt;p&gt;This is a surprisingly elegant solution, because it matches roughly how people think. No one is going to read a dozen books cover to cover to answer a question. We continuously narrow our scope until we find enough information to answer. &lt;/p&gt;&lt;p&gt;This approach gives your AI model the same capability to narrowly target the information it needs to answer the user&amp;rsquo;s query efficiently and quickly.&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>https://ayende.com/blog/203430-C/using-multi-staged-actions-with-ai-agents-to-reduce-costs-time?Key=38244b26-20b7-4900-9b80-4b105cea21b2</link><guid>https://ayende.com/blog/203430-C/using-multi-staged-actions-with-ai-agents-to-reduce-costs-time?Key=38244b26-20b7-4900-9b80-4b105cea21b2</guid><pubDate>Mon, 17 Nov 2025 12:00:00 GMT</pubDate></item><item><title>Reducing AI context load using actions</title><description>&lt;p&gt;When using an AI model, one of the things that you need to pay attention to is the number of tokens you send to the model. They &lt;em&gt;literally&lt;/em&gt;&amp;nbsp;cost you money, so you have to balance the amount of data you send to the model against how much of it is &lt;em&gt;relevant&lt;/em&gt;&amp;nbsp;to what you want it to do.&lt;/p&gt;&lt;p&gt;That is especially important when you are building generic agents, which may be assigned a &lt;em&gt;bunch&lt;/em&gt;&amp;nbsp;of different tasks. The classic example is the human resources assistant, which may be tasked with checking your vacation days balance or called upon to get the current number of overtime hours that an employee has worked this month.&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s assume that we want to provide the model with a bit of context. We want to give the model all the recent HR tickets by the current employee. These can range from onboarding tasks to filling out the yearly evaluation, etc. &lt;/p&gt;&lt;p&gt;That sounds like it can give the model a big hand in understanding the state of the employee and what they want. Of course, that assumes the user is going to ask a question related to those issues.&lt;/p&gt;&lt;p&gt;What if they ask about the date of the next bank holiday? If we just unconditionally fed all the data to the model preemptively, that would be:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Quite confusing to the model, since it will have to sift through a lot of irrelevant data.&lt;/li&gt;&lt;li&gt;Pretty expensive, since we&amp;rsquo;re going to send a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of data (and pay for it) to the model, which then has to ignore it.&lt;/li&gt;&lt;li&gt;Compounding effect as the user &amp;amp; the model keep the conversation going, with all this unneeded information weighing everything down.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A nice trick that can really help is to &lt;em&gt;not &lt;/em&gt;expose the data directly, but rather provide it to the model as a set of actions it can invoke. In other words, when defining the agent, I don&amp;rsquo;t bother providing it with all the data it needs.&lt;/p&gt;&lt;p&gt;Rather, I provide the model a &lt;em&gt;way to access the data&lt;/em&gt;. Here is what this looks like in RavenDB:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/IAtwhrjTjb9youuoOByI3Q.png"/&gt;&lt;/p&gt;&lt;p&gt;The agent is provided with a bunch of queries that it can call to find out various interesting details about the current employee. The end result is that the model will invoke those queries to get &lt;em&gt;just&lt;/em&gt;&amp;nbsp;the information it wants.&lt;/p&gt;&lt;p&gt;The overall number of tokens that we are going to consume will be &lt;em&gt;greatly&lt;/em&gt;&amp;nbsp;reduced, while the ability of the model to actually access relevant information is enhanced. We don&amp;rsquo;t need to go through stuff we don&amp;rsquo;t care about, after all. &lt;/p&gt;&lt;p&gt;This approach gives you a very focused model for the task at hand, and it is easy to&amp;nbsp;extend the agent with additional information-retrieval capabilities.&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>https://ayende.com/blog/203429-C/reducing-ai-context-load-using-actions?Key=34b3dc56-9e87-445e-972d-348f105e80aa</link><guid>https://ayende.com/blog/203429-C/reducing-ai-context-load-using-actions?Key=34b3dc56-9e87-445e-972d-348f105e80aa</guid><pubDate>Fri, 14 Nov 2025 12:00:00 GMT</pubDate></item><item><title>Using AI Agents parameters outside of the model's scope</title><description>&lt;p&gt;Building an AI Agent in RavenDB is very much like defining a class, you define all the things that it can do, the initial prompt to the AI model, and you specify which parameters the agent requires. Like a class, you can create an instance of an AI agent by starting a new conversation with it. Each conversation is a separate instance of the agent, with different parameters, an initial user prompt, and its own history.&lt;/p&gt;&lt;p&gt;Here is a simple example of a non-trivial agent. For the purpose of this post, I want to focus on the parameters that we pass to the model. &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 agent &lt;span class="token operator"&gt;=&lt;/span&gt; new AiAgentConfiguration&lt;span class="token punctuation"&gt;(&lt;/span&gt;
&lt;span class="token string"&gt;"shopping assistant"&lt;/span&gt;, 
config.ConnectionStringName,
&lt;span class="token string"&gt;"You are an AI agent of an online shop..."&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    Parameters &lt;span class="token operator"&gt;=&lt;/span&gt;
    &lt;span class="token punctuation"&gt;[&lt;/span&gt; 
       new AiAgentParameter&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"lang"&lt;/span&gt;, 
&lt;span class="token string"&gt;"The language the model should respond with."&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;,
        new AiAgentParameter&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"currency"&lt;/span&gt;, &lt;span class="token string"&gt;"Preferred currency for the user"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;,
        new AiAgentParameter&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"customerId"&lt;/span&gt;, null, sendToModel: &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;,
    Queries &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt; /* redacted&lt;span class="token punctuation"&gt;..&lt;/span&gt;. */ &lt;span class="token punctuation"&gt;]&lt;/span&gt;,
    Actions &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt; /* redacted&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 in the configuration, we define the &lt;code&gt;lang&lt;/code&gt;&amp;nbsp;and &lt;code&gt;currency&lt;/code&gt;&amp;nbsp;parameters as standard agent parameters. These are defined with a description for the model and are passed to the model when we create a new conversation. &lt;/p&gt;&lt;p&gt;But what about the &lt;code&gt;customerId&lt;/code&gt;&amp;nbsp;parameter? It is marked as &lt;code&gt;sendToModel: false&lt;/code&gt;. What is the &lt;em&gt;point&lt;/em&gt;&amp;nbsp;of that? To understand this, you need to know a bit more about how RavenDB deals with the model, conversations, and memory.&lt;/p&gt;&lt;p&gt;Each conversation with the model is recorded using a conversation document, and part of this includes the parameters you pass to the conversation when you create it. In this case, we don&amp;rsquo;t need to pass the &lt;code&gt;customerId&lt;/code&gt;&amp;nbsp;parameter to the model; it doesn&amp;rsquo;t hold any meaning for the model and would just waste tokens.&lt;/p&gt;&lt;p&gt;The key is that you can &lt;em&gt;query based &lt;/em&gt;on those parameters. For example, if you want to get all the conversations for a particular customer (to show them their conversation history), you can use the following query:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-php'&gt;&lt;code class='line-numbers language-php'&gt;from &lt;span class="token string double-quoted-string"&gt;"@conversations"&lt;/span&gt; 
where Parameters&lt;span class="token operator"&gt;.&lt;/span&gt;customerId &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token variable"&gt;$customerId&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;This is also &lt;em&gt;very&lt;/em&gt;&amp;nbsp;useful when you have data that you genuinely don&amp;rsquo;t want to expose to the model but still want to attach to the conversation. You can set up a query that the model may call to get the most recent orders for a customer, and RavenDB will do that (using &lt;code&gt;customerId&lt;/code&gt;) without letting the model actually see that value. &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>https://ayende.com/blog/203428-C/using-ai-agents-parameters-outside-of-the-models-scope?Key=9fb498ed-4de8-473a-a3e6-78221253f0c6</link><guid>https://ayende.com/blog/203428-C/using-ai-agents-parameters-outside-of-the-models-scope?Key=9fb498ed-4de8-473a-a3e6-78221253f0c6</guid><pubDate>Wed, 12 Nov 2025 12:00:00 GMT</pubDate></item><item><title>The cost of design iteration in software engineering</title><description>&lt;p&gt;I ran into this tweet from about &lt;a href="https://x.com/thdxr/status/1964481163575321081"&gt;a month ago&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://x.com/thdxr"&gt;dax &lt;/a&gt;&lt;/strong&gt;&lt;a href="https://x.com/thdxr"&gt;@thdxr&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;programmers have a dumb chip on their shoulder that makes them try and emulate traditional engineering there is zero physical cost to iteration in software - can delete and start over, can live patch our approach should look a lot different than people who build bridges&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I have to say that I would &lt;em&gt;strongly &lt;/em&gt;disagree with this statement.&amp;nbsp;Using&amp;nbsp;the building example, it is obvious that moving a window in an already built house is expensive. &lt;em&gt;Obviously,&lt;/em&gt;&amp;nbsp;it is going to be cheaper to move this window during the planning phase. &lt;/p&gt;&lt;p&gt;The answer is that it may be cheap&lt;em&gt;er&lt;/em&gt;, but it won&amp;rsquo;t necessarily be &lt;em&gt;cheap&lt;/em&gt;. Let&amp;rsquo;s say that I want to move the window by 50 cm to the right. Would it be up to code? Is there any wiring that needs to be moved? Do I need to consider the placement of the air conditioning unit? What about the emergency escape? Any structural impact?&lt;/p&gt;&lt;p&gt;This is when we are at the blueprint stage - the equivalent of editing code on screen. And it is obvious that such changes can be &lt;em&gt;really&lt;/em&gt;&amp;nbsp;expensive. Similarly, in software, every modification demands a careful assessment of the existing system, long-term maintenance, compatibility with other components, and user expectations.This intricate balancing act is at the core of the engineering discipline.&lt;/p&gt;&lt;p&gt;A civil engineer designing a bridge faces tangible constraints: the physical world, regulations, budget limitations, and environmental factors like wind, weather, and earthquakes.While software designers might not grapple with physical forces, they contend with equally critical elements such as disk usage, data distribution,&amp;nbsp;rules &amp;amp; regulations, system usability, operational procedures, and the impact of expected future changes.&lt;/p&gt;&lt;p&gt;Evolving an existing software system presents a substantial engineering challenge.Making significant modifications without causing the system to collapse requires careful planning and execution.The notion that one can simply &amp;quot;start over&amp;quot; or &amp;quot;live deploy&amp;quot; changes is incredibly risky.History is replete with examples of major worldwide outages stemming from seemingly simple configuration changes.A notable instance is &lt;a href="https://status.cloud.google.com/incidents/ow5i3PPK96RduMcb1SsW"&gt;the Google outage of June 2025&lt;/a&gt;, where a simple missing null check brought down significant portions of GCP. Even small alterations can have cascading and catastrophic effects.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m currently working on a codebase whose age is near the legal drinking age. It also has close to 1.5 million lines of code and a big team operating on it. Being able to successfully run, maintain, and extend that over time requires discipline.&lt;/p&gt;&lt;p&gt;In such a project, you face issues such as different versions of the software deployed in the field, backward compatibility concerns, etc. For example, I may have a better idea of how to structure the data to make a particular scenario more efficient. That would require updating the on-disk data, which is a 100% engineering challenge. We have to take into consideration physical constraints (updating a multi-TB dataset without downtime is a tough challenge).&lt;/p&gt;&lt;p&gt;The moment you are actually deployed, you have &lt;em&gt;so many &lt;/em&gt;additional concerns to deal with. A good example of this may be that users are &lt;em&gt;used&lt;/em&gt;&amp;nbsp;to &lt;a href="https://xkcd.com/1172/"&gt;stuff working in a certain way&lt;/a&gt;. But even for software that hasn&amp;rsquo;t been deployed to production yet, the cost of change is &lt;em&gt;high&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;Consider the effort associated with this update to a &lt;code&gt;JobApplication&lt;/code&gt;&amp;nbsp;class:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/Q6aV54vBOui5OWp4Pb1ABA.png"/&gt;&lt;/p&gt;&lt;p&gt;This &lt;em&gt;looks&lt;/em&gt;&amp;nbsp;like a simple change, right? It just requires that you (partial list):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Set up database migration for the new shape of the data.&lt;/li&gt;&lt;li&gt;Migrate the &lt;em&gt;existing &lt;/em&gt;data to the new format.&lt;/li&gt;&lt;li&gt;Update any indexes and queries on the position.&lt;/li&gt;&lt;li&gt;Update any endpoints and decide how to deal with backward compatibility.&lt;/li&gt;&lt;li&gt;Create a new user interface to match this whenever we create/edit/view the job application.&lt;/li&gt;&lt;li&gt;Consider any existing workflows that inherently assume that a job application is for a &lt;em&gt;single&lt;/em&gt;&amp;nbsp;position. &lt;/li&gt;&lt;li&gt;Can you be &lt;em&gt;partially&lt;/em&gt;&amp;nbsp;rejected? What is your status if you interviewed for one position but received an offer for another?&lt;/li&gt;&lt;li&gt;How does this affect the reports &amp;amp; dashboard? &lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is a &lt;em&gt;simple&lt;/em&gt;&amp;nbsp;change, no? Just a few characters on the screen. No physical cost. But it is also a full-blown Epic Task for the project - even if we aren&amp;rsquo;t in production, have no data to migrate, or integrations to deal with.&lt;/p&gt;&lt;p&gt;Software engineersoperate under constraints similar to other engineers, including severe consequences for mistakes (global system failure because of a missing null check). Making changes to large, established codebases presents a significant hurdle.&lt;/p&gt;&lt;p&gt;The moment that you need to consider more than a single factor, whether in your code or in a bridge blueprint, there &lt;em&gt;is&lt;/em&gt;&amp;nbsp;a pretty high cost to iterations. Going back to the bridge example, the architect may have a rough idea (is it going to be a Roman-style arch bridge or a suspension bridge) and have a lot of freedom to play with various options at the start. But the moment you begin to nail things down and fill in the details, the cost of change escalates quickly.&lt;/p&gt;&lt;p&gt;Finally, just to be clear, I don&amp;rsquo;t think that the cost of changing software is equivalent to changing a bridge after it was built. I simply very strongly disagree that there is zero cost (or indeed, even &lt;em&gt;low&lt;/em&gt;&amp;nbsp;cost) to changing software once you are past the &amp;ldquo;rough draft&amp;rdquo; stage.&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>https://ayende.com/blog/203364-C/the-cost-of-design-iteration-in-software-engineering?Key=619cc0e6-43da-46ff-9cb8-e1ef309e61e1</link><guid>https://ayende.com/blog/203364-C/the-cost-of-design-iteration-in-software-engineering?Key=619cc0e6-43da-46ff-9cb8-e1ef309e61e1</guid><pubDate>Mon, 13 Oct 2025 12:00:00 GMT</pubDate></item><item><title>Using AI for candidate ranking with RavenDB</title><description>&lt;p&gt;Hiring the right people is notoriously difficult.I have been personally involved in hiring decisions for about two decades, and it&amp;nbsp;is an unpleasant process. You deal with an utterly overwhelming influx of applications, often from&amp;nbsp;candidates using the &amp;ldquo;spray and pray&amp;rdquo; approach of applying to &lt;em&gt;all&lt;/em&gt;&amp;nbsp;jobs.&lt;/p&gt;&lt;p&gt;At one point, I got the resume of a &lt;em&gt;divorce lawyer&lt;/em&gt;&amp;nbsp;in response to a job posting for a backend engineer role. I was curious enough to follow up on that, and no, that lawyer didn&amp;rsquo;t want to change careers. He was interested in &lt;em&gt;being&lt;/em&gt;&amp;nbsp;a divorce lawyer. What kind of clients would want their divorce handled by a database company, I refrained from asking.&lt;/p&gt;&lt;p&gt;Companies often resort to &lt;em&gt;expensive &lt;/em&gt;external agencies to sift through countless candidates.&lt;/p&gt;&lt;p&gt;In the age of AI and LLMs, is that still the case? This post will demonstrate how to build an intelligent candidate screening process using RavenDB and modern AI, enabling you to efficiently accept applications, match them to appropriate job postings, and make an initial go/no-go decision for your recruitment pipeline.&lt;/p&gt;&lt;p&gt;We&amp;rsquo;ll start our process by defining a couple of open positions:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Staff Engineer, Backend &amp;amp; DevOps&lt;/li&gt;&lt;li&gt;Senior Frontend Engineer (React/TypeScript/SaaS)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Here is what this looks like at the database level:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/fx1KoGukCHzVhmX1K0tVIg.png"/&gt;&lt;/p&gt;&lt;p&gt;Now, let&amp;rsquo;s create a couple of applicants for those positions. We have James &amp;amp; Michael, and they look like this:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/WfowzB467778q8-oXKFP1Q.png"/&gt;&lt;/p&gt;&lt;p&gt;Note that we are not actually doing a lot here in terms of the data we ask the applicant to provide. We mostly gather the contact information and ask them to attach their resume. You can see the resume attachment in RavenDB Studio. In the above screenshot, it is in the right-hand Attachments pane of the document view.&lt;/p&gt;&lt;p&gt;Now we can use RavenDB&amp;rsquo;s new &lt;a href="https://github.com/ravendb/ravendb/discussions/21526"&gt;Gen AI attachments feature&lt;/a&gt;. I defined an OpenAI connection with &lt;code&gt;gpt-4.1-mini&lt;/code&gt;&amp;nbsp;and created a Gen AI task to read &amp;amp; understand the resume. I&amp;rsquo;m assuming that you&amp;rsquo;ve read my post about &lt;a href="https://ayende.com/blog/202851-C/ravendb-7-1-the-gen-ai-release"&gt;Gen AI in RavenDB&lt;/a&gt;, so I&amp;rsquo;ll&amp;nbsp;skip going over the actual setup.&lt;/p&gt;&lt;p&gt;The key is that I&amp;rsquo;m applying the following context extraction script to the &lt;code&gt;Applicants &lt;/code&gt;collection:&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;const&lt;/span&gt; resumePdf &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;loadAttachment&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;"resume.pdf"&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;if&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;!&lt;/span&gt;resumePdf&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;


ai&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;genContext&lt;/span&gt;&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; &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;applicantName&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;withPdf&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;resumePdf&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;When I test this script on James&amp;rsquo;s document, I get:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/urBuojUpKDTA2Zjz1MA5_g.png"/&gt;&lt;/p&gt;&lt;p&gt;Note that we have the attachment in the bottom right - that will &lt;em&gt;also&lt;/em&gt;&amp;nbsp;be provided to the model. So we can now write the following prompt for the model:&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;You are an HR data parsing specialist. Your task is to analyze the provided CV/resume content &lt;span class="token punctuation"&gt;(&lt;/span&gt;from the PDF&lt;span class="token punctuation"&gt;)&lt;/span&gt; 
and extract the candidate's professional profile into the provided JSON schema.
In the requiredTechnologies object, every value within the arrays &lt;span class="token punctuation"&gt;(&lt;/span&gt;languages, frameworks_libraries, etc.&lt;span class="token punctuation"&gt;)&lt;/span&gt; must be a single, 
distinct technology or concept. Do not use slashes &lt;span class="token punctuation"&gt;(&lt;/span&gt;/&lt;span class="token punctuation"&gt;)&lt;/span&gt;, commas, semicolons, or parentheses &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; to combine items within a single string. Separate combined concepts into individual strings &lt;span class="token punctuation"&gt;(&lt;/span&gt;e.g., &lt;span class="token string"&gt;"Ruby/Rails"&lt;/span&gt; becomes &lt;span class="token string"&gt;"Ruby"&lt;/span&gt;, &lt;span class="token string"&gt;"Rails"&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 also ask the model to respond with an object matching the following sample:&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;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token property"&gt;"location"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"The primary location or if interested in remote option (e.g., Pasadena, CA or Remote)"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token property"&gt;"summary"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"A concise overview of the candidate's history and key focus areas (e.g., Lead development of data-driven SaaS applications focusing on React, TypeScript, and Usability)."&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token property"&gt;"coreResponsibilities"&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"&gt;"A list of the primary duties and contributions in previous roles."&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 property"&gt;"requiredTechnologies"&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 property"&gt;"languages"&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"&gt;"Key programming and markup languages that the candidate has experience with."&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 property"&gt;"frameworks_libraries"&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"&gt;"Essential UI, state management, testing, and styling libraries."&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 property"&gt;"tools_platforms"&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"&gt;"Version control, cloud platforms, build tools, and project management systems."&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 property"&gt;"data_storage"&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"&gt;"The database technologies the candidate is expected to work with."&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;Testing this on James&amp;rsquo;s applicant document results in the following output:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/oUGu2IC2HbXp0Uc7LO0bwQ.png"/&gt;&lt;/p&gt;&lt;p&gt;I actually had to check where the model got the &amp;ldquo;LA Canada&amp;rdquo; issue. That &lt;em&gt;shows up&lt;/em&gt;&amp;nbsp;in the real resume PDF, and it is a real place. I triple-checked, because I was sure this was a hallucination at first &amp;#9786;&amp;#65039;.&lt;/p&gt;&lt;p&gt;The last thing we need to do is actually deal with the model&amp;rsquo;s output. We use an update script to apply the model&amp;rsquo;s output to the document. In this case, it is as simple as just storing it in the source document:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-php'&gt;&lt;code class='line-numbers language-php'&gt;this&lt;span class="token operator"&gt;.&lt;/span&gt;resume &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token variable"&gt;$output&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 here is what the output looks like:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/Nff9I0NkT-SZnbFzYEMuWg.png"/&gt;&lt;/p&gt;&lt;p&gt;Reminder: Gen AI tasks in RavenDB use a three-stage approach:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Context extraction script - gets data (and attachment) from the source document to provide to the model.&lt;/li&gt;&lt;li&gt;Prompt &amp;amp; Schema - instructions for the model, telling it what it should &lt;em&gt;do&lt;/em&gt;&amp;nbsp;with the provided context and how it should format the output.&lt;/li&gt;&lt;li&gt;Update script - takes the structured output from the model and applies it back to the source document.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In our case, this process starts with the applicant uploading their CV, and then we have the &lt;code&gt;Read Resume&lt;/code&gt;&amp;nbsp;task running. This parses the PDF and puts the result in the document, which is great, but it is only part of the process.&lt;/p&gt;&lt;p&gt;We now have the resume contents in a structured format, but we need to &lt;em&gt;evaluate&lt;/em&gt;&amp;nbsp;the candidate&amp;rsquo;s suitability for all the positions they applied for. We are going to do that using the model &lt;em&gt;again&lt;/em&gt;, with a new Gen AI task.&lt;/p&gt;&lt;p&gt;We start by defining the following context extraction script:&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;// wait until the resume (parsed CV) has been added to the document&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;&lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;resume&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 keyword"&gt;for&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; positionId of &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;targetPosition&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; position &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;load&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;positionId&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;position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token keyword"&gt;continue&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    ai&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;genContext&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
        position&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        positionId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        resume&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;resume
    &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 this relies on the &lt;code&gt;resume &lt;/code&gt;field that we created in the previous task. In other words, we set things up in such a way that we run this task &lt;em&gt;after&lt;/em&gt;&amp;nbsp;the &lt;code&gt;Read Resume&lt;/code&gt;&amp;nbsp;task, but without needing to put them in an explicit pipeline or manage their execution order.&lt;/p&gt;&lt;p&gt;Next, note that we output &lt;em&gt;multiple &lt;/em&gt;contexts for the same document. Here is what this looks like for James, we have two &lt;em&gt;separate&lt;/em&gt;&amp;nbsp;contexts, one for each position James applied for:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/pMhfqVPKDiUQtdWoNCDpSg.png"/&gt;&lt;/p&gt;&lt;p&gt;This is important because we want to process each position and resume &lt;em&gt;independently&lt;/em&gt;. This avoids context leakage from one position to another. It also lets us process multiple positions for the same applicant &lt;em&gt;concurrently&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;Now, we need to tell the model what it is supposed to do:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-lua'&gt;&lt;code class='line-numbers language-lua'&gt;You are a specialized HR Matching AI&lt;span class="token punctuation"&gt;.&lt;/span&gt; Your task is to receive two structured JSON objects — one describing a Job Position &lt;span class="token keyword"&gt;and&lt;/span&gt; one 
summarizing a Candidate Resume — &lt;span class="token keyword"&gt;and&lt;/span&gt; evaluate the suitability of the resume &lt;span class="token keyword"&gt;for&lt;/span&gt; the position&lt;span class="token punctuation"&gt;.&lt;/span&gt;


Assess the overlap &lt;span class="token keyword"&gt;in&lt;/span&gt; jobTitle&lt;span class="token punctuation"&gt;,&lt;/span&gt; summary&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token keyword"&gt;and&lt;/span&gt; coreResponsibilities&lt;span class="token punctuation"&gt;.&lt;/span&gt; Does the candidate&lt;span class="token string"&gt;'s career trajectory align with the role'&lt;/span&gt;s &lt;span class="token function"&gt;needs&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;e&lt;span class="token punctuation"&gt;.&lt;/span&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; has matching experience required &lt;span class="token keyword"&gt;for&lt;/span&gt; a Senior Frontend role&lt;span class="token punctuation"&gt;)&lt;/span&gt;?
Technical Match&lt;span class="token punctuation"&gt;:&lt;/span&gt; Compare the technologies listed &lt;span class="token keyword"&gt;in&lt;/span&gt; the requiredTechnologies sections&lt;span class="token punctuation"&gt;.&lt;/span&gt; Identify both direct &lt;span class="token function"&gt;matches&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;must&lt;span class="token operator"&gt;-&lt;/span&gt;haves&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token keyword"&gt;and&lt;/span&gt; &lt;span class="token function"&gt;gaps&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;missing &lt;span class="token keyword"&gt;or&lt;/span&gt; weak areas&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt; Consider substitutions such as js &lt;span class="token keyword"&gt;or&lt;/span&gt; ecmascript to javascript &lt;span class="token keyword"&gt;or&lt;/span&gt; node&lt;span class="token punctuation"&gt;.&lt;/span&gt;js&lt;span class="token punctuation"&gt;.&lt;/span&gt; 


Evaluate &lt;span class="token keyword"&gt;if&lt;/span&gt; the candidate's experience level &lt;span class="token keyword"&gt;and&lt;/span&gt; domain &lt;span class="token function"&gt;expertise&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;e&lt;span class="token punctuation"&gt;.&lt;/span&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; SaaS&lt;span class="token punctuation"&gt;,&lt;/span&gt; Data Analytics&lt;span class="token punctuation"&gt;,&lt;/span&gt; Mapping Solutions&lt;span class="token punctuation"&gt;)&lt;/span&gt; meet &lt;span class="token keyword"&gt;or&lt;/span&gt; exceed the requirements&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 the output schema that we want to get from the model is:&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;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token property"&gt;"explanation"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"Provide a detailed analysis here. Start by confirming the high-level match (e.g., 'The candidate is an excellent match because...'). Detail the strongest technical overlaps (e.g., React, TypeScript, Redux, experience with BI/SaaS). Note any minor mismatches or significant overqualifications (e.g., candidate's deep experience in older technologies like ASP.NET classic is not required but demonstrates full-stack versatility)."&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;  &lt;span class="token property"&gt;"isSuitable"&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;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;Here I want to stop for a moment and talk about what exactly we are doing here. We &lt;em&gt;could&lt;/em&gt;&amp;nbsp;ask the model just to judge whether an applicant is suitable for a position and save a bit on the number of tokens we spend. However, getting just a yes/no response from the model is not something I recommend.&lt;/p&gt;&lt;p&gt;There are two primary reasons why we want the explanation field as well. First, it serves as a good check on the model itself. The &lt;em&gt;order&lt;/em&gt;&amp;nbsp;of properties matters in the output schema. We first ask the model to explain itself, then to render the verdict. That means it is going to be more focused. &lt;/p&gt;&lt;p&gt;The other reason is a bit more delicate. You may be &lt;em&gt;required&lt;/em&gt;&amp;nbsp;to provide an explanation to the applicant if you reject them. I won&amp;rsquo;t necessarily put this exact justification in the rejection letter to the applicant, but it is something that is quite important to retain in case you need to provide it later.&lt;/p&gt;&lt;p&gt;Going back to the task itself, we have the following update script:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-php'&gt;&lt;code class='line-numbers language-php'&gt;this&lt;span class="token operator"&gt;.&lt;/span&gt;suitability &lt;span class="token operator"&gt;=&lt;/span&gt; this&lt;span class="token operator"&gt;.&lt;/span&gt;&lt;span class="token class-name"&gt;suitability&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 punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
this&lt;span class="token operator"&gt;.&lt;/span&gt;suitability&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token variable"&gt;$input&lt;/span&gt;&lt;span class="token operator"&gt;.&lt;/span&gt;positionId&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token variable"&gt;$output&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;Here we are doing something quite interesting. We extracted the &lt;code&gt;positionId&lt;/code&gt;&amp;nbsp;at the start of this process, and we are using it to associate the output from the model with the specific position we are currently evaluating.&lt;/p&gt;&lt;p&gt;Note that we are actually evaluating multiple positions for the same applicant at the same time, and we need to execute this update script for &lt;em&gt;each&lt;/em&gt;&amp;nbsp;of them. So we need to ensure that we don&amp;rsquo;t overwrite previous work. &lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I&amp;rsquo;m not mentioning this in detail because I covered it in &lt;a href="https://ayende.com/blog/202851-C/ravendb-7-1-the-gen-ai-release"&gt;my previous Gen AI post&lt;/a&gt;, but it is important to note that we have &lt;em&gt;two&lt;/em&gt;&amp;nbsp;tasks sourced from the same document. RavenDB knows how to handle the data being modified by both tasks without triggering an infinite loop. It seems like a small thing, but it is the sort of thing that &lt;em&gt;not&lt;/em&gt;&amp;nbsp;having to worry about really simplifies the whole process.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;With these two tasks, we have now set up a complete pipeline for the initial processing of applicants to open positions. As you can see here:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/UJye4HLxVnussV8N7hMA_g.png"/&gt;&lt;/p&gt;&lt;p&gt;This sort of process allows you to integrate into your system stuff that, until recently, looked like science fiction. A pipeline like the one above is not something you could just build before, but now you can spend a few hours and have this capability ready to deploy.&lt;/p&gt;&lt;p&gt;Here is what the tasks look like inside RavenDB:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/q73Fk5q3NfPuM4rkEVYhyg.png"/&gt;&lt;/p&gt;&lt;p&gt;And the final applicant document after all of them have run is:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/I5ERv44KePhzPaVoGsdwLw.png"/&gt;&lt;/p&gt;&lt;p&gt;You can see the metadata for the two tasks (which we use to avoid going to the model again when we don&amp;rsquo;t have to), as well as the actual outputs of the model (&lt;code&gt;resume&lt;/code&gt;, &lt;code&gt;suitability&lt;/code&gt;&amp;nbsp;fields).&lt;/p&gt;&lt;p&gt;A few more notes before we close this post. I chose to use two GenAI tasks here, one to read the resume and generate the structured output, and the second to actually evaluate the applicant&amp;rsquo;s suitability.&lt;/p&gt;&lt;p&gt;From a modeling perspective, it is easier to split this into distinct steps. You &lt;em&gt;can&lt;/em&gt;&amp;nbsp;ask the model to both read the resume and evaluate suitability in a single shot, but I find that it makes it harder to extend the system down the line.&lt;/p&gt;&lt;p&gt;Another reason you want to have different tasks for this is that you can use &lt;em&gt;different&lt;/em&gt;&amp;nbsp;models for each one. For example, reading the resume and extracting the structured output is something you can run on &lt;code&gt;gpt-4.1-mini&lt;/code&gt;&amp;nbsp;or &lt;code&gt;gpt-5-nano,&lt;/code&gt;&amp;nbsp;while evaluating applicant suitability can make use of a smarter model.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m really happy with the new RavenDB AI integration features. We got some early feedback that is &lt;em&gt;really&lt;/em&gt;&amp;nbsp;exciting, and I&amp;rsquo;m looking forward to seeing what you can do with them.&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>https://ayende.com/blog/203363-C/using-ai-for-candidate-ranking-with-ravendb?Key=e63b9a8f-6547-4b36-ab7a-f634938a8e09</link><guid>https://ayende.com/blog/203363-C/using-ai-for-candidate-ranking-with-ravendb?Key=e63b9a8f-6547-4b36-ab7a-f634938a8e09</guid><pubDate>Fri, 10 Oct 2025 12:00:00 GMT</pubDate></item><item><title>When perf optimization breaks tests in a GOOD way</title><description>&lt;p&gt;You might have noticed a theme going on in RavenDB. We &lt;em&gt;care&lt;/em&gt;&amp;nbsp;a lot about performance. The problem with optimizing performance is that sometimes you have a great idea, you implement it, the performance gains are &lt;em&gt;there&lt;/em&gt;&amp;nbsp;to be had - and then a test fails&amp;hellip; and you realize that your great idea now needs to be 10 times more complex to handle a niche edge case.&lt;/p&gt;&lt;p&gt;We did a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of work around optimizing the performance of RavenDB at the lowest levels for the next major release (8.0), and we got a persistently failing test that we started to look at.&lt;/p&gt;&lt;p&gt;Here is the failing message:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Restore with MaxReadOpsPerSecond = 1 should take more than &amp;#39;11&amp;#39; seconds, but it took &amp;#39;00:00:09.9628728&amp;#39;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The test in question is &lt;code&gt;ShouldRespect_Option_MaxReadOpsPerSec_OnRestore&lt;/code&gt;, part of the &lt;code&gt;MaxReadOpsPerSecOptionTests &lt;/code&gt;suite of tests. What it tests is that we can &lt;em&gt;limit &lt;/em&gt;how fast RavenDB can restore a database. &lt;/p&gt;&lt;p&gt;The reason you want to do that is to avoid consuming too many system resources when performing a big operation. For example, I may want to restore a &lt;em&gt;big&lt;/em&gt;&amp;nbsp;database, but I don&amp;rsquo;t want to consume all the IOPS on the server, because there are additional databases running on it.&lt;/p&gt;&lt;p&gt;At any rate, we started to get test failures on this test. And a deeper investigation revealed something quite amusing. We made the entire system more efficient. In particular, we managed to reduce the size of the buffers used significantly, so we can push more data faster. It turns out that this is enough to break the test.&lt;/p&gt;&lt;p&gt;The fix was to reduce the actual time that we budget as the minimum viable time. And I have to say that this is one of those pull requests that lights a warm fire in my heart.&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>https://ayende.com/blog/203331-C/when-perf-optimization-breaks-tests-in-a-good-way?Key=593060c0-2e46-43d4-bb16-f6cb89abd738</link><guid>https://ayende.com/blog/203331-C/when-perf-optimization-breaks-tests-in-a-good-way?Key=593060c0-2e46-43d4-bb16-f6cb89abd738</guid><pubDate>Tue, 07 Oct 2025 12:00:00 GMT</pubDate></item><item><title>Cryptographic documents in RavenDB</title><description>&lt;p&gt;We got an interesting use case from a customer - they need to &lt;em&gt;verify&lt;/em&gt;&amp;nbsp;that documents in RavenDB have not been modified by any external party, including users with administrator credentials for the database.&lt;/p&gt;&lt;p&gt;This is known as the Rogue Root problem, where you have to protect yourself from potentially malicious root users. That is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;an easy problem - in theory, you can safeguard yourself &lt;a href="https://techcommunity.microsoft.com/blog/exchange/protecting-against-rogue-administrators/585155"&gt;using various means&lt;/a&gt;, for example the whole premise of SELinux is based on that. &lt;/p&gt;&lt;p&gt;I don&amp;rsquo;t really like that approach, since I assume that if a user has (valid) root access, they also likely have &lt;em&gt;physical access&lt;/em&gt;. In other words, they can change the operating system to bypass any hurdles in the way.&lt;/p&gt;&lt;p&gt;Luckily, the scenario we were presented with involved detecting changes made by an administrator, which is significantly easier. And we can also use some cryptography tools to help us handle even the case of detecting &lt;em&gt;malicious&lt;/em&gt;&amp;nbsp;tampering.&lt;/p&gt;&lt;p&gt;First, I&amp;rsquo;m going to show how to make this work with RavenDB, then we&amp;rsquo;ll discuss the implications of this approach for the overall security of the system.&lt;/p&gt;&lt;h1&gt;The implementation&lt;/h1&gt;&lt;p&gt;The RavenDB client API allows you to hook into the saving process of documents, as you can see in the code below. In this example, I&amp;rsquo;m using a user-specific &lt;code&gt;ECDsa &lt;/code&gt;key (by calling the &lt;code&gt;GetSigningKeyForUser()&lt;/code&gt;&amp;nbsp;method).&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;store&lt;span class="token punctuation"&gt;.&lt;/span&gt;OnBeforeStore &lt;span class="token operator"&gt;+=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;sender&lt;span class="token punctuation"&gt;,&lt;/span&gt; e&lt;/span&gt;&lt;span class="token punctuation"&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; obj &lt;span class="token operator"&gt;=&lt;/span&gt; e&lt;span class="token punctuation"&gt;.&lt;/span&gt;Session&lt;span class="token punctuation"&gt;.&lt;/span&gt;JsonConverter&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToBlittable&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;e&lt;span class="token punctuation"&gt;.&lt;/span&gt;Entity&lt;span class="token punctuation"&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;var&lt;/span&gt; date &lt;span class="token operator"&gt;=&lt;/span&gt; DateTime&lt;span class="token punctuation"&gt;.&lt;/span&gt;UtcNow&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"&gt;"O"&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; data &lt;span class="token operator"&gt;=&lt;/span&gt; Encoding&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;UTF8&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetBytes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt; e&lt;span class="token punctuation"&gt;.&lt;/span&gt;DocumentId &lt;span class="token operator"&gt;+&lt;/span&gt; date &lt;span class="token operator"&gt;+&lt;/span&gt; obj&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    
    using ECDsa key &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;GetSigningKeyForUser&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;CurrentUser&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; signData &lt;span class="token operator"&gt;=&lt;/span&gt; key&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;SignData&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;data&lt;span class="token punctuation"&gt;,&lt;/span&gt; HashAlgorithmName&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;SHA256&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    e&lt;span class="token punctuation"&gt;.&lt;/span&gt;DocumentMetadata&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"DigitalSignature"&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 keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;Dictionary&lt;/span&gt;&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;string&lt;span class="token punctuation"&gt;,&lt;/span&gt; string&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;"User"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; CurrentUser&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"Signature"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; Convert&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToBase64String&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signData&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 string"&gt;"Date"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; date&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"PublicKey"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; key&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ExportSubjectPublicKeyInfoPem&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 punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;What you can see here is that we are using the user&amp;rsquo;s key to generate a signature that is composed of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The document&amp;rsquo;s ID.&lt;/li&gt;&lt;li&gt;The current signature time.&lt;/li&gt;&lt;li&gt;The JSON content of the entity.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;After we generate the signature, we add it to the document&amp;rsquo;s metadata. This allows us to verify that the entity is indeed valid and was signed by the proper user. &lt;/p&gt;&lt;p&gt;To validate this afterward, we use 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;bool ValidateEntity&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token constant"&gt;T&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;IAsyncDocumentSession session&lt;span class="token punctuation"&gt;,&lt;/span&gt;&lt;span class="token constant"&gt;T&lt;/span&gt; entity&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; metadata &lt;span class="token operator"&gt;=&lt;/span&gt; session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Advanced&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetMetadataFor&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;entity&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; documentId &lt;span class="token operator"&gt;=&lt;/span&gt; session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Advanced&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetDocumentId&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;entity&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; digitalSignature &lt;span class="token operator"&gt;=&lt;/span&gt; metadata&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetObject&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"DigitalSignature"&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 keyword"&gt;throw&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;IOException&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"Signature is missing for "&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; documentId&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; date &lt;span class="token operator"&gt;=&lt;/span&gt; digitalSignature&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetString&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"Date"&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; user &lt;span class="token operator"&gt;=&lt;/span&gt; digitalSignature&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetString&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"User"&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; signature &lt;span class="token operator"&gt;=&lt;/span&gt; digitalSignature&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetString&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"Signature"&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; key &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;GetPublicKeyForUser&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;user&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; obj &lt;span class="token operator"&gt;=&lt;/span&gt; session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Advanced&lt;span class="token punctuation"&gt;.&lt;/span&gt;JsonConverter&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToBlittable&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;entity&lt;span class="token punctuation"&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;var&lt;/span&gt; data &lt;span class="token operator"&gt;=&lt;/span&gt; Encoding&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;UTF8&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GetBytes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;documentId &lt;span class="token operator"&gt;+&lt;/span&gt; date &lt;span class="token operator"&gt;+&lt;/span&gt; obj&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; bytes &lt;span class="token operator"&gt;=&lt;/span&gt; Convert&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;FromBase64String&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signature&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; key&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;VerifyData&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;data&lt;span class="token punctuation"&gt;,&lt;/span&gt; bytes&lt;span class="token punctuation"&gt;,&lt;/span&gt; HashAlgorithmName&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;SHA256&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 here, too, we are using the &lt;code&gt;GetPublicKeyForUser&lt;/code&gt;&lt;code&gt;()&lt;/code&gt;&amp;nbsp;to get the proper public key to validate the signature. We use the specified user from the metadata to get the key, and we verify the signature against the document ID, the date in the metadata, and the JSON of the entity.&lt;/p&gt;&lt;p&gt;We are also saving the &lt;em&gt;public &lt;/em&gt;key of the signing user in the metadata. But we haven&amp;rsquo;t used it so far, why are we doing this? &lt;/p&gt;&lt;p&gt;The reason we use &lt;code&gt;GetPublicKeyForUser()&lt;/code&gt;&amp;nbsp;in the &lt;code&gt;ValidateEntity()&lt;/code&gt;&amp;nbsp;call is pretty simple: we want to get the user&amp;rsquo;s key from the same source. This assumes that the user&amp;rsquo;s key is stored in a safe location (a secure vault or a hardware key like YubiKey, etc.). &lt;/p&gt;&lt;p&gt;The reason we want to store the public key in the metadata is so we can verify the data on the &lt;em&gt;server&lt;/em&gt;&amp;nbsp;side. I created the following index:&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;from c &lt;span class="token keyword"&gt;in&lt;/span&gt; docs&lt;span class="token punctuation"&gt;.&lt;/span&gt;Companies
&lt;span class="token keyword"&gt;let&lt;/span&gt; unverified &lt;span class="token operator"&gt;=&lt;/span&gt; Crypto&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Verify&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;c&lt;span class="token punctuation"&gt;)&lt;/span&gt;
where unverified is not &lt;span class="token keyword"&gt;null&lt;/span&gt;
select &lt;span class="token keyword"&gt;new&lt;/span&gt; 
&lt;span class="token punctuation"&gt;{&lt;/span&gt; 
    Problem &lt;span class="token operator"&gt;=&lt;/span&gt; unverified
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m using &lt;a href="https://docs.ravendb.net/7.1/indexes/extending-indexes/"&gt;RavenDB&amp;rsquo;s additional sources&lt;/a&gt;&amp;nbsp;feature to add &lt;a href="https://gist.github.com/ayende/050c1a9dd6a484a53354f3d41efa9464"&gt;the following code&lt;/a&gt;&amp;nbsp;to the index. This exposes the &lt;code&gt;Crypto.Verify()&lt;/code&gt;&amp;nbsp;call to the index, and the code uses the public key in the metadata (as well as the other information there) to verify that the document signature is valid.&lt;/p&gt;&lt;p&gt;The index code above will filter all the documents whose signature is valid, so you can easily get all the problematic documents. In other words, it is a quick way of saying: &amp;ldquo;Find me all the documents whose verification failed&amp;rdquo;. For compliance, that is quite important and usually requires going over the entire dataset to answer it.&lt;/p&gt;&lt;h1&gt;The implications&lt;/h1&gt;&lt;p&gt;Let&amp;rsquo;s consider the impact of such a system. We now have cryptographic verification that the document was modified by a specific user. Any tampering with the document will invalidate the digital signature (or require signing it with your key).&lt;/p&gt;&lt;p&gt;Combine that with &lt;a href="https://docs.ravendb.net/7.0/document-extensions/revisions/overview/"&gt;RavenDB&amp;rsquo;s revisions&lt;/a&gt;, and you have an immutable log that you can verify using modern cryptography. No, it isn&amp;rsquo;t a blockchain, but it will put a significant roadblock in the path of anyone trying to just modify the data. &lt;/p&gt;&lt;p&gt;The fact that we do the signing on the client side, rather than the server, means that the server never actually has &lt;em&gt;access&lt;/em&gt;&amp;nbsp;to the signing keys (only the public keys). The server&amp;rsquo;s administrator, in the same manner, doesn&amp;rsquo;t have a way to get those signing keys and forge a document.&lt;/p&gt;&lt;p&gt;In other words, we solved the Rogue Root problem, and we ensured that a user cannot repudiate a document they signed. It is easy to audit the system for invalid documents (and, combined with revisions, go back to a valid one).&lt;/p&gt;&lt;h3&gt;Escape hatch design&lt;/h3&gt;&lt;p&gt;If you need this sort of feature for compliance only, you may want to skip the &lt;code&gt;ValidateEntity()&lt;/code&gt;&amp;nbsp;call. That would allow an administrator to manually change a document (thus invalidating the digital signature) and still have the rest of the system work. That goes against what we are trying to do, yes, but it is sometimes desirable.&lt;/p&gt;&lt;p&gt;That isn&amp;rsquo;t required for the normal course of operations, but it &lt;em&gt;can&lt;/em&gt;&amp;nbsp;be required for troubleshooting, for example. I&amp;rsquo;m sure you can think of a number of reasons why it would make things a lot easier to fix if you could just modify the database&amp;rsquo;s data.&lt;/p&gt;&lt;p&gt;For example, an &lt;code&gt;Order&lt;/code&gt;&amp;nbsp;contains a &lt;code&gt;ZipCode&lt;/code&gt;&amp;nbsp;with the value &lt;code&gt;&amp;quot;02116&amp;quot;&lt;/code&gt;&amp;nbsp;(note the leading zero), which a downstream system turns into the integer &lt;code&gt;02116&lt;/code&gt;. An administrator can change the value to be &lt;code&gt;&amp;quot; 02116&amp;quot;&lt;/code&gt;, with a leading space, preventing this problem (the downstream system will not convert this to a number, thus keeping the leading 0). Silly, yes - but it happens all the time.&lt;/p&gt;&lt;p&gt;Even though we are invalidating the digital signature, we may want to do that anyway. The index we defined would alert on this, but we can proceed with processing the order, then fix it up later. Or just make a note of this for compliance purposes. &lt;/p&gt;&lt;h1&gt;Summary&lt;/h1&gt;&lt;p&gt;This post walks you through building a cryptographic solution to protect document integrity within a RavenDB environment, addressing the Rogue Root problem. The core mechanism is a client-side &lt;code&gt;OnBeforeStore &lt;/code&gt;hook that generates an &lt;code&gt;ECDsa&lt;/code&gt;&amp;nbsp;digital signature for each document. This design ensures that the private keys are never exposed on the server, preventing a database administrator from forging signatures and providing true non-repudiation.&lt;/p&gt;&lt;p&gt;A RavenDB index is used to automatically and asynchronously verify every document&amp;#39;s signature against its current content. This index filters for any documents where the digital signature is valid, providing an efficient server-side audit mechanism to find all the documents with invalid signatures.&lt;/p&gt;&lt;p&gt;The really fun part here is that there isn&amp;rsquo;t really a lot of code or complexity involved, and you get strong cryptographic proof that your data has not been tampered with.&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>https://ayende.com/blog/203300-B/cryptographic-documents-in-ravendb?Key=913acc58-fb7d-4f8f-bccd-298ff20858ca</link><guid>https://ayende.com/blog/203300-B/cryptographic-documents-in-ravendb?Key=913acc58-fb7d-4f8f-bccd-298ff20858ca</guid><pubDate>Fri, 26 Sep 2025 12:00:00 GMT</pubDate></item><item><title>Recording: How To Create Powerful and Secure AI Agents with RavenDB</title><description>&lt;p&gt;&lt;iframe width="1840" height="1035" title="[LIVE] COD#6 How To Create Powerful and Secure AI Agents with RavenDB CEO, Oren Eini | RavenDB" src="https://www.youtube.com/embed/jzUxL9P17G4" frameborder="0" allowfullscreen="" referrerpolicy="strict-origin-when-cross-origin" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"&gt;&lt;/iframe&gt;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;Unlock practical AI agents inside your database. In this live demo and deep dive, Oren Eini shows how to build real, production-ready AI agents directly in RavenDB that query your data, take actions, remember context, and stay inside strict security guardrails. You will see an agent defined in a few lines of code, connected to OpenAI or any LLM you choose, running vector search and RAG over your catalog, and safely executing business actions like “add to cart,” “find policies,” or “sign document,” all with parameters that are enforced by the database rather than trusted to the model. You will learn how RavenDB agents eliminate fragile glue code by giving the model explicit tools: data queries that return typed results and server-side actions you validate in your code. &lt;/p&gt;&lt;p&gt;Conversations are stored as documents, with automatic token-aware summarization to control latency and cost. The demo streams responses token by token for responsive UX, switches models without rewrites, and shows how scope parameters prevent data leaks even if the prompt is manipulated. You will also see a multi-tool HR assistant that chains tools, coordinates front end and back end, and persists state. The session closes with a look at the roadmap, including multi-agent orchestration and AI assist inside Studio.&lt;/p&gt;</description><link>https://ayende.com/blog/203267-A/recording-how-to-create-powerful-and-secure-ai-agents-with-ravendb?Key=4f5a9a1c-96bc-4607-aced-64991f6d0927</link><guid>https://ayende.com/blog/203267-A/recording-how-to-create-powerful-and-secure-ai-agents-with-ravendb?Key=4f5a9a1c-96bc-4607-aced-64991f6d0927</guid><pubDate>Mon, 22 Sep 2025 12:00:00 GMT</pubDate></item><item><title>Scheduling with RavenDB</title><description>&lt;p&gt;I got a question from one of our users about how they can use RavenDB to manage scheduled tasks. Stuff like: &amp;ldquo;Send this email next Thursday&amp;rdquo; or &amp;ldquo;Cancel this reservation if the user didn&amp;rsquo;t pay within 30 minutes.&amp;rdquo;&lt;/p&gt;&lt;p&gt;As you can tell from the context, this is both more straightforward and more complex than the &amp;ldquo;run this every 2nd Wednesday&amp;quot; you&amp;rsquo;ll typically encounter when talking about scheduled jobs.&lt;/p&gt;&lt;p&gt;The answer for how to do that in RavenDB is pretty simple, you use &lt;a href="https://docs.ravendb.net/7.1/server/extensions/refresh"&gt;the Document Refresh feature&lt;/a&gt;. This is a really &lt;em&gt;tiny &lt;/em&gt;feature when you consider what it does. Given this document:&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;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
   &lt;span class="token property"&gt;"Redacted"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"Details"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
   &lt;span class="token property"&gt;"@metdata"&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 property"&gt;"@collection"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"RoomAvailabilities"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
      &lt;span class="token property"&gt;"@refresh"&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"2025-09-14T10:00:00.0000000Z"&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;RavenDB will remove the &lt;code&gt;@refresh &lt;/code&gt;metadata field at the specified time. That is &lt;em&gt;all&lt;/em&gt;&amp;nbsp;this does, nothing else. That looks like a pretty useless feature, I admit, but there is a point to it.&lt;/p&gt;&lt;p&gt;The act of removing the &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;field from the document will also (obviously) update the document, which means that everything that reacts to a document update will also react to this.&lt;/p&gt;&lt;p&gt;&lt;a href="https://www.ayende.com/blog/195393-A/ravendb-subscriptions-messaging-patterns"&gt;I wrote about this in the past&lt;/a&gt;, but it turns out there are a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of interesting things you can do with this. For example, consider the following index definition:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-go'&gt;&lt;code class='line-numbers language-go'&gt;from RoomAvailabilitiesas r
where &lt;span class="token boolean"&gt;true&lt;/span&gt; and not &lt;span class="token function"&gt;exists&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token string"&gt;"@metadata"&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token string"&gt;"@refresh"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token keyword"&gt;select&lt;/span&gt; &lt;span class="token builtin"&gt;new&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; 
  r&lt;span class="token punctuation"&gt;.&lt;/span&gt;RoomId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  r&lt;span class="token punctuation"&gt;.&lt;/span&gt;Date&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token comment"&gt;// etc...&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 you see here is an index that lets me &amp;ldquo;hide&amp;rdquo; documents (that were reserved) until that reservation expires. &lt;/p&gt;&lt;p&gt;I can do quite a lot with this feature. For example, use this in RabbitMQ ETL to build automatic delayed sending of documents. Let&amp;rsquo;s implement a &amp;ldquo;dead-man switch&amp;rdquo;, a document will be automatically sent to a RabbitMQ channel if a server doesn&amp;rsquo;t contact us often enough:&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;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;'@metadata'&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;"@refresh"&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 punctuation"&gt;;&lt;/span&gt; &lt;span class="token comment"&gt;// no need to send if refresh didn't expire&lt;/span&gt;


&lt;span class="token keyword"&gt;var&lt;/span&gt; alertData &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token literal-property property"&gt;Id&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function"&gt;id&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;this&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 literal-property property"&gt;ServerId&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;ServerId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token literal-property property"&gt;LastUpdate&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;Timestamp&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token literal-property property"&gt;LastStatus&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token keyword"&gt;this&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;Status &lt;span class="token operator"&gt;||&lt;/span&gt; &lt;span class="token string"&gt;'ACTIVE'&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;loadToAlertExchange&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;alertData&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;'alert.operations'&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 literal-property property"&gt;Id&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function"&gt;id&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;this&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 literal-property property"&gt;Type&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'operations.alerts.missing_heartbeat'&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token literal-property property"&gt;Source&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'/operations/server-down/no-heartbeat'&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 is that whenever a server contacts us, we&amp;rsquo;ll update the &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;field to the maximum duration we are willing to miss updates from the server. If that time expires, RavenDB will remove the &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;field, and the RabbitMQ ETL script will send an alert to the RabbitMQ exchange. You&amp;rsquo;ll note that this is actually reacting to &lt;em&gt;inaction&lt;/em&gt;, which is a surprisingly hard thing to actually do, usually.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;You&amp;rsquo;ll notice that, like many things in RavenDB, most features tend to be small and focused. The idea is that they compose well together and let you build the behavior you need with a very low complexity threshold.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The common use case for &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;is when you use &lt;a href="https://docs.ravendb.net/7.1/client-api/data-subscriptions/what-are-data-subscriptions/"&gt;RavenDB Data Subscriptions&lt;/a&gt;&amp;nbsp;to process documents. For example, you want to send an email in a week. This is done by writing an EmailToSend document with a &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;of a week from now and defining a subscription with the following query:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-julia'&gt;&lt;code class='line-numbers language-julia'&gt;from EmailToSend as e
where &lt;span class="token boolean"&gt;true&lt;/span&gt; and not exists&lt;span class="token punctuation"&gt;(&lt;/span&gt;e&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token operator"&gt;'&lt;/span&gt;@metadata&lt;span class="token operator"&gt;'&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token operator"&gt;'&lt;/span&gt;@refresh&lt;span class="token operator"&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 simply filter out those that have a &lt;code&gt;@refresh &lt;/code&gt;field, it&amp;rsquo;s that simple. Then, in your code, you can ignore the scheduling aspect entirely. Here is what this looks like:&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; subscription &lt;span class="token operator"&gt;=&lt;/span&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;Subscriptions
    .GetSubscriptionWorker&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;EmailToSend&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;"EmailToSendSubscription"&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;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;subscription&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Run&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;async&lt;/span&gt; batch &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; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;batch&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&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;
    foreach &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;var&lt;/span&gt; item &lt;span class="token keyword"&gt;in&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;batch&lt;span class="token punctuation"&gt;.&lt;/span&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; email &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;item&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Result&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;EmailProvider.SendEmailAsync&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;EmailMessage&lt;/span&gt;
        &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; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;email&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;To&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;Subject&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;email&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Subject&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;Body&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;email&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Body&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; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"no-reply@example.com"&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;&lt;span class="token namespace"&gt;email&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&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;"Sent"&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;email&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;SentAt&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 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 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 nothing in this code handles scheduling. RavenDB is in charge of sending the documents to the subscription when the time expires.&lt;/p&gt;&lt;p&gt;Using &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;+ Subscriptions in this manner provides us with a number of interesting advantages:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Missed Triggers&lt;/strong&gt;:&amp;nbsp;Handles missed schedules seamlessly, resuming on the next subscription run.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reliability&lt;/strong&gt;: Automatically retries subscription processing on errors.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rescheduling&lt;/strong&gt;: When &lt;code&gt;@refresh &lt;/code&gt;expires, your subscription worker will get the document and can decide to act or reschedule a check by updating the &lt;code&gt;@refresh&lt;/code&gt;&amp;nbsp;field again.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Robustness&lt;/strong&gt;: You can rely on RavenDB to keep serving subscriptions even if nodes (both clients &amp;amp; servers) fail.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Scaleout&lt;/strong&gt;: You can use concurrent subscriptions to have multiple workers read from the same subscription.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can take this approach really far, in terms of load, throughput, and complexity. The nice thing about this setup is that you don&amp;rsquo;t need to glue together cron, a message queue, and worker management. You can let RavenDB handle it all for you.&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>https://ayende.com/blog/203203-B/scheduling-with-ravendb?Key=bec80bdd-3afc-4a81-97ab-c83f0c0e4955</link><guid>https://ayende.com/blog/203203-B/scheduling-with-ravendb?Key=bec80bdd-3afc-4a81-97ab-c83f0c0e4955</guid><pubDate>Thu, 18 Sep 2025 12:00:00 GMT</pubDate></item><item><title>Webinar: Building AI Agents in RavenDB</title><description>&lt;p&gt;Tomorrow I&amp;rsquo;ll be giving a webinar on &lt;a href="https://meeting.ravendb.net/meeting/register?sessionId=1067947396&amp;src=174f21bf2eb9b8b149be34be8a206a4b2d5638f353c2dc527cec42efcbd87faa"&gt;Building AI Agents in RavenDB&lt;/a&gt;. I&amp;rsquo;m going to show off some really cool ways to apply AI agents on your data, as well as our approach to AI and LLM in general.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m looking forward to seeing you there. &lt;/p&gt;&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt;&amp;nbsp;This is going to blow your mind. &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>https://ayende.com/blog/203235-C/webinar-building-ai-agents-in-ravendb?Key=d4bb653d-c9b6-4a79-80b2-aea99c190a45</link><guid>https://ayende.com/blog/203235-C/webinar-building-ai-agents-in-ravendb?Key=d4bb653d-c9b6-4a79-80b2-aea99c190a45</guid><pubDate>Tue, 16 Sep 2025 12:00:00 GMT</pubDate></item></channel></rss>