<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Metabase | Business Intelligence, Dashboards, and Data Visualization</title>
    <description>Metabase is the easy, open-source way for everyone to ask questions and learn from data.</description>
    <link>https://www.metabase.com/</link>
    <atom:link href="https://www.metabase.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Wed, 13 May 2026 22:11:02 +0000</pubDate>
    <lastBuildDate>Wed, 13 May 2026 22:11:02 +0000</lastBuildDate>
    <generator>Jekyll v4.3.3</generator>
    
      <item>
        <title>Winners of the Metabase AI Hackathon</title>
        <description>&lt;p&gt;We ran the Metabase AI Hackathon to celebrate every AI feature in Metabase going open source.&lt;/p&gt;

&lt;p&gt;The submissions were genuinely fun to go through: analytics agents, new products, an entire API with machine learning, a couple of things that made us laugh, a couple that made us think “oh, that’s actually clever.” Picking two was harder than we expected.&lt;/p&gt;

&lt;p&gt;Here are the winners:&lt;/p&gt;

&lt;h2 id=&quot;meta-chess-by-marat-surmashev&quot;&gt;Meta Chess, by Marat Surmashev&lt;/h2&gt;

&lt;p&gt;A live dashboard where Claude and Codex play chess against each other, with Metabase as the entire game platform, not a passive viewer.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/metachess.png&quot; alt=&quot;Meta Chess&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This one uses two of our newest features in tandem. The agents sync through the &lt;a href=&quot;/docs/latest/ai/mcp&quot;&gt;MCP server&lt;/a&gt;: read the board state, check whose turn it is, and see the opponent’s last move. What spectators watching the dashboard see and what the agents see is the same source of truth.&lt;/p&gt;

&lt;p&gt;The dashboard itself is built with &lt;a href=&quot;/docs/latest/ai/file-based-development&quot;&gt;file-based development&lt;/a&gt;: the live chessboard, the last-move highlight, and the move history are all Metabase YAML cards shipped through the serialization v2 API.&lt;/p&gt;

&lt;p&gt;We picked this one because it shows off two of our newest features doing something neither was designed for. The MCP server isn’t just feeding one agent, it’s the message bus between two. File-based development isn’t just for version control, it’s how the whole dashboard ships as code. Turns out a Metabase dashboard can be a chessboard, a referee, and a livestream all at once. Who knew.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.linkedin.com/posts/marat-surmashev_metabaseai-metabase-llm-ugcPost-7456645398351273984-3DsZ/&quot;&gt;See the project&lt;/a&gt; · &lt;a href=&quot;https://github.com/Aitem/metachess&quot;&gt;GitHub repo&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;claudes-advice-by-owais-mumtaz&quot;&gt;Claude’s Advice, by Owais Mumtaz&lt;/h2&gt;

&lt;p&gt;A nightly Cowork task that reads the last 60 days of Owais’s personal fitness data via our MCP server, reviews his goals, and writes back to his dashboard as a chart titled “Claude’s Advice.”&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/claude-advice-metabase-ai-hackathon-winner.png&quot; alt=&quot;Claude&apos;s Advice&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Calories, macros, workouts, weight — all facts and dimensions in a personal database, with a semantic layer and predefined metrics. When Metabase 60 dropped MCP support, Owais realized Claude could use what he’d already built. The data was already mapped. Based on his current plateau, the chart told him to refeed this weekend: 3,000 calories, 400g carbs, hit legs hard, check the scale Wednesday.&lt;/p&gt;

&lt;p&gt;We picked this one because it’s small, useful, and real. No big architecture diagram, no fancy multi-agent setup. Just one nightly task, one trusted semantic layer, and one chart that tells Owais what to do tomorrow. The kind of project that a lot of people could build for themselves this weekend… and probably should.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.linkedin.com/posts/owais-mumtaz_metabaseai-ugcPost-7457300849795469312-fdQ1/&quot;&gt;Watch Owais’s walking through the project
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two mechanical keyboards are on their way to Marat and Owais. Thanks to everyone who participated.&lt;/p&gt;

&lt;p&gt;If you haven’t tried &lt;a href=&quot;/docs/latest/ai/start&quot;&gt;AI in Metabase&lt;/a&gt; yet, you don’t need a hackathon to start, go build something and tag us, we read everything&lt;/p&gt;
</description>
        <pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/metabase-ai-hackathon-winners</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/metabase-ai-hackathon-winners</guid>
        
        
        <category>News</category>
        
      </item>
    
      <item>
        <title>Improving the performance of the popular Clojure development tool clojure-lsp</title>
        <description>&lt;p&gt;Development tools can sometimes struggle when dealing with large codebases. This gives performance nerds like me a reason to investigate. In this case, I ended up cutting clojure-lsp’s startup time in half and memory allocation by two thirds.&lt;/p&gt;

&lt;h2 id=&quot;part-1-the-mystery-of-heap-headroom&quot;&gt;Part 1: The mystery of heap headroom&lt;/h2&gt;

&lt;p&gt;Devs working on Metabase were complaining about LSP taking too long to boot, so I wondered how long it could be. A few seconds? Half a minute? Imagine my surprise when I saw this:&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure-lsp.api/analyze-project-only!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:project-root&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure.java.io/file&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/path/to/metabase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Elapsed time: 178981.918417 msecs&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Three minutes is a long time.&lt;/p&gt;

&lt;p&gt;The first suspect was heap size. There is a good rule of a thumb: if some process in Clojure (or any JDK language) takes longer to complete than anticipated, or doesn’t finish at all, you should check the heap.&lt;/p&gt;

&lt;p&gt;I used &lt;a href=&quot;https://visualvm.github.io/&quot;&gt;VisualVM&lt;/a&gt; to inspect our Clojure process and ran the benchmarking command, which gave us something like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/engineering-2026/heap-usage-before.png&quot; alt=&quot;Profile of heap usage before changes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What’s going on here? My laptop has 16 GB of RAM. By default, Java will take 25% of total RAM as the maximum heap size for the process. In my case, that means 4GB max heap, as you see in the screenshot. Clojure-lsp’s analysis fills up the heap with more and more unfreeable data, meaning objects that don’t get garbage-collected. The heap headroom (the amount of free heap space after garbage collection) becomes smaller and smaller, which means garbage collection has to run more often, to the point where a GC run has to start up before the last run can complete.&lt;/p&gt;

&lt;p&gt;Giving more heap space to the process by adding an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-J-Xmx6g&lt;/code&gt; argument to the REPL command line shows a different picture:&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure-lsp.api/analyze-project-only!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:project-root&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure.java.io/file&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/path/to/metabase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Elapsed time: 115370.613041 msecs&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/engineering-2026/heap-usage-after.png&quot; alt=&quot;Profile of heap usage before changes, showing much more headroom&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Still almost two minutes, but we’ve shaved off a minute by just giving the process more space to breathe. The heap plot shows that “used heap” no longer hugs the “max heap” threshold so tightly, so GCs trigger less often.&lt;/p&gt;

&lt;h2 id=&quot;part-2-profiling-and-optimizations&quot;&gt;Part 2: Profiling and optimizations&lt;/h2&gt;

&lt;p&gt;My next step was to use &lt;a href=&quot;https://github.com/clojure-goes-fast/clj-async-profiler&quot;&gt;clj-async-profiler&lt;/a&gt; to obtain a flamegraph of the benchmark above. I took an &lt;a href=&quot;https://clojure-goes-fast.com/kb/profiling/clj-async-profiler/allocation-profiling/&quot;&gt;allocation profile&lt;/a&gt; instead of a regular CPU profile because I’ve found that optimizing allocation hotspots results in roughly the same execution time savings, but allocation profiles have less variance and skew.&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clj-async-profiler.core/profile&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:event&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure-lsp.api/analyze-project-only!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:project-root&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure.java.io/file&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/path/to/metabase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;center&gt;
&lt;figure&gt;
&lt;div style=&quot;height:400px; width: 150%; -moz-transform: scale(0.666); -moz-transform-origin: 0 0; -o-transform: scale(0.666); -o-transform-origin: 0 0; -webkit-transform: scale(0.666); -webkit-transform-origin: 0 0;&quot;&gt;
&lt;iframe src=&quot;https://flamebin.dev/8QyDP4&quot; style=&quot;height:600px; width:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    Allocation flamegraph before improvements. &lt;a href=&quot;https://flamebin.dev/ciWxK6&quot; target=&quot;_blank&quot;&gt;Click to open&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/center&gt;

&lt;p&gt;A flamegraph is a good way to spot the hot code in stack traces (see &lt;a href=&quot;https://clojure-goes-fast.com/kb/profiling/clj-async-profiler/exploring-flamegraphs/&quot;&gt;this tutorial&lt;/a&gt; for more on flamegraphs). On the Y axis, you have stacks that grow from bottom to top. The position of the frame signifies which function calls which, and the height of the flamegraph shows the stack’s depth (which usually doesn’t matter).&lt;/p&gt;

&lt;p&gt;On the X-axis, the width of a frame shows how much total time each function call takes (or, in this case, the number of allocations of a particular object class). The coordinate of the frame on the X-axis doesn’t imply its &lt;em&gt;position&lt;/em&gt; in time; in fact, the X-axis coordinate doesn’t mean anything (just its width).&lt;/p&gt;

&lt;p&gt;Looking at the flamegraph, most of clojure-lsp’s initialization is taken up by &lt;a href=&quot;https://github.com/clj-kondo/clj-kondo&quot;&gt;clj-kondo&lt;/a&gt;. If you search in the embedded graph for “kondo,” you’ll find that 95.41% of all frames match, meaning almost all work is kondo-related.&lt;/p&gt;

&lt;p&gt;After massaging the flamegraph and pruning irrelevant pieces, I found several significant inefficiencies and solutions for them.&lt;/p&gt;

&lt;h3 id=&quot;deep-merge&quot;&gt;deep-merge&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/clj-kondo/clj-kondo/pull/2779&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deep-merge&lt;/code&gt;&lt;/a&gt; function is similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clojure.core/merge&lt;/code&gt;, but when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deep-merge&lt;/code&gt; merges maps, it also merges values with duplicate keys in those maps. There were a few things to be improved here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the value in the resulting map doesn’t change, don’t perform the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assoc&lt;/code&gt; operation. So if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; already has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:k1 someval}&lt;/code&gt;, we can skip &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(assoc m :k1 someval)&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Before merging two sets, check if they’re equal. If they are, there’s no need to merge them; just return one of them.&lt;/li&gt;
  &lt;li&gt;Only use Clojure transients for large collections, as transients bear some static upfront overhead, which can be less efficient than using simple vectors for small collections.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;rewrite-clj-improvements&quot;&gt;rewrite-clj improvements&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clj-commons/rewrite-clj&quot;&gt;rewrite-clj&lt;/a&gt; is a library that Kondo and LSP use to read and parse Clojure. I’ve applied several optimizations to it, but the most impactful change was dropping a &lt;a href=&quot;https://github.com/clj-commons/rewrite-clj&quot;&gt;dynamic variable&lt;/a&gt; on a hot path.&lt;/p&gt;

&lt;p&gt;In Clojure, dynamic variables (usually &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*earmuffed*&lt;/code&gt;) allow for passing “hidden” context to other functions instead of adding explicit arguments. In a sense, this hidden context is global state, but restricted to stack scope. A simple example:&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:dynamic&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;*honorific*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;*honorific*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;*honorific*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;*honorific*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Mrs.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Smith&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Mrs. Smith&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, having &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*honorific*&lt;/code&gt; as a dynamic variable saved us the trouble of adding another argument to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello&lt;/code&gt;. But setting a value for a dynamic variable with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;binding&lt;/code&gt; involves creating a new hashmap every time, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assoc&lt;/code&gt;-ing the value to it. That means allocations and spent cycles. So, if you care about the performance of a particular hot function, make sure to pass all arguments explicitly.&lt;/p&gt;

&lt;h3 id=&quot;better-memoization&quot;&gt;Better memoization&lt;/h3&gt;

&lt;p&gt;Clojure kindly provides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clojure.core/memoize&lt;/code&gt; function, which caches the outputs of a function for the given inputs.&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoize&lt;/code&gt;’s implementation, however, is quite simplistic and unoptimized:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;To accept functions of &lt;em&gt;any&lt;/em&gt; arity, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clojure.core/memoize&lt;/code&gt; uses a list of arguments as the cache key. Rolling arguments into the list triggers additional allocations.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memoize&lt;/code&gt; also uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; to look up the value for the key in the cache. When the map contains the key, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; returns a key-value pair, a MapEntry object, which again needs to be allocated in the heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because clj-kondo only needs memoization for one- and two-argument functions, &lt;a href=&quot;https://github.com/clj-kondo/clj-kondo/pull/2780&quot;&gt;a more specialized implementation&lt;/a&gt; was warranted. For a single argument, the code is straightforward. For two arguments, the main trick is to structure the cache as a nested map &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{arg1 {arg2 cached-value}}&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{[arg1 arg2] cached-value}&lt;/code&gt;. Nesting the map avoids wrapping arguments into a vector during the lookup.&lt;/p&gt;

&lt;h2 id=&quot;part-3-measuring-total-allocations&quot;&gt;Part 3: Measuring total allocations&lt;/h2&gt;

&lt;p&gt;I proposed each optimization as a separate PR to clj-kondo, because I thought it would be helpful for the maintainer, the venerable &lt;a href=&quot;https://github.com/borkdude&quot;&gt;Michiel Borkent&lt;/a&gt;, to see the impact of each optimization. The timing differences for each improvement separately were too flaky to observe a meaningful difference, so I came up with another metric: the total number of allocated bytes during the benchmark.&lt;/p&gt;

&lt;p&gt;The following script does the work of hooking into GC events and captures how many bytes were freed each time. In the end, we add up all those bytes to calculate the total allocated value.&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;com.sun.management&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GarbageCollectionNotificationInfo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GcInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;javax.management&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NotificationEmitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java.lang.management&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ManagementFactory&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MemoryUsage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;javax.management&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NotificationListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
 &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;javax.management.openmbean&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CompositeData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memory-bean&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ManagementFactory/getMemoryMXBean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-collections&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;atom&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calc-freed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Return the number of bytes reclaimed by a given GC run.&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GcInfo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getMemoryUsageBeforeGc&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getMemoryUsageAfterGc&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getUsed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MemoryUsage&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-after&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getUsed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MemoryUsage&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;install-gc-listener!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Add a hook that runs on every GC invocation to intercept and remember how
  much heap space the GC freed.&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;doseq&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-bean&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ManagementFactory/getGarbageCollectorMXBeans&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.addNotificationListener&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
     &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NotificationEmitter&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-bean&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
     &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reify&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NotificationListener&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
       &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;handleNotification&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
         &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getType&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GarbageCollectionNotificationInfo/GARBAGE_COLLECTION_NOTIFICATION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
           &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GarbageCollectionNotificationInfo/from&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                       &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CompositeData&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getUserData&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                 &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-info&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getGcInfo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                 &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;calc-freed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
             &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;swap!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-collections&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;conj&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
     &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;install-gc-listener!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-heap-before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getUsed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getHeapMemoryUsage&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memory-bean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure-lsp.api/analyze-project-only!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;:project-root&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clojure.java.io/file&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/path/to/metabase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;print-allocation-stats&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-heap-after&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getUsed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.getHeapMemoryUsage&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memory-bean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total-allocated&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-collections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                           &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-heap-after&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                           &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;used-heap-before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Allocation stats: %s GC collections, allocated %.1fGB&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                     &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc-collections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                     &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total-allocated&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))))))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;print-allocation-stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With this script, I got the following numbers before and after the applied optimizations:&lt;/p&gt;

&lt;div class=&quot;language-clojure highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;;; Before&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Elapsed time: 89335.263292 msecs&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Allocation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;245&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GC&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collections,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;allocated&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;145.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GB&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; After&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Elapsed time: 46784.39225 msecs&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Allocation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;117&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GC&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collections,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;allocated&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;51.6&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GB&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We see a 2x improvement in the elapsed time and almost 3x reduction of the number of bytes allocated on the heap.&lt;/p&gt;

&lt;p&gt;To see where exactly the improvements manifested in the code, I took a second allocation profile using clj-async-profiler to generate a &lt;a href=&quot;https://clojure-goes-fast.com/kb/profiling/clj-async-profiler/diffgraphs/&quot;&gt;diffgraph&lt;/a&gt;.&lt;/p&gt;

&lt;center&gt;
&lt;figure&gt;
&lt;div style=&quot;height:400px; width: 150%; -moz-transform: scale(0.666); -moz-transform-origin: 0 0; -o-transform: scale(0.666); -o-transform-origin: 0 0; -webkit-transform: scale(0.666); -webkit-transform-origin: 0 0;&quot;&gt;
&lt;iframe src=&quot;https://flamebin.dev/qBGwfE&quot; style=&quot;height:600px; width:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    Allocation diffgraph after improvements. &lt;a href=&quot;https://flamebin.dev/qBGwfE&quot; target=&quot;_blank&quot;&gt;Click to open.&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/center&gt;

&lt;p&gt;A diffraph is a type of flamegraph that combines two profiles and shows differences between them. Color represents the direction and intensity of change. Blue frames mean that the second profile contains fewer samples for that code path (which means faster, fewer allocations, etc.). Red frames show more samples than before.&lt;/p&gt;

&lt;p&gt;Highlighting the diffgraph’s base shows that we reduced allocations by ~66% (which matches the benchmark results above). If you scroll up to stacks of more saturated blue, you’ll see which parts of the code were the most affected. For instance, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clj-kondo.impl.analyzer.namespace/analyze-ns-decl&lt;/code&gt; has almost completely disappeared from the profile (-96% allocations on that codepath), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clj-kondo.impl.rewrite-clj.parser.core/parse-delim&lt;/code&gt; performs 93% fewer allocations than before, and so on. When you see a sea of blue on the diffgraph, you know you are doing something right.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Being able to do something in a development tool is the main part, but speed and ergonomics matter just as much. A tool that is “almost instant” when employed in a small project may become unwieldy in an industrial-scale one like the Metabase codebase.&lt;/p&gt;

&lt;p&gt;We significantly improved clojure-lsp’s initialization time and memory pressure. &lt;a href=&quot;https://github.com/ericdallo&quot;&gt;Eric Dallo&lt;/a&gt;, the maintainer of clojure-lsp, has kindly provided &lt;a href=&quot;https://github.com/clj-kondo/clj-kondo/issues/2778&quot;&gt;his own benchmark results and analysis of the optimizations&lt;/a&gt;, and has independently confirmed the speed-ups on codebases comparable to Metabase. Our proposed changes are already merged, and will ship in the new release of clojure-lsp.&lt;/p&gt;

&lt;p&gt;There is still plenty to be done to improve the clojure-lsp experience. The server still keeps a weighty amount of heap when loaded (around 2-3 GB) and there are probably a lot more opportunities to reduce this number. But that’s a story for another blog post.&lt;/p&gt;
</description>
        <pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/improving-performance-clojure-development-tools</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/improving-performance-clojure-development-tools</guid>
        
        
        <category>Engineering</category>
        
      </item>
    
      <item>
        <title>How we built ten custom subagents to tame a 500K-line Clojure codebase</title>
        <description>&lt;script type=&quot;application/ld+json&quot;&gt;
{
   &quot;@context&quot;:&quot;https://schema.org&quot;,
   &quot;@type&quot;:&quot;Article&quot;,
   &quot;@id&quot;:&quot;https://www.metabase.com/blog/ten-custom-subagents&quot;,
   &quot;url&quot;:&quot;https://www.metabase.com/blog/ten-custom-subagents&quot;,
   &quot;headline&quot;:&quot;How we built ten custom subagents to tame a 500K-line Clojure codebase&quot;,
   &quot;description&quot;:&quot;We built ten domain-expert subagents to manage context when working on Metabase&apos;s 500K+ line backend.&quot;,
   &quot;image&quot;:&quot;https://www.metabase.com/images/posts/engineering-2026/ten-custom-agents.png&quot;,
   &quot;datePublished&quot;:&quot;2026-03-27T08:00:00+08:00&quot;,
   &quot;dateModified&quot;:&quot;2026-03-27T08:00:00+08:00&quot;,
   &quot;author&quot;:[
      {
         &quot;@type&quot;:&quot;Person&quot;,
         &quot;name&quot;:&quot;Bryan Mass&quot;,
         &quot;url&quot;:&quot;https://www.metabase.com/blog&quot;
      }
   ]
}
&lt;/script&gt;

&lt;p&gt;Metabase’s backend is big. We’re talking 500K lines of Clojure code spread across a query processor, permissions system, numerous database drivers, a notification pipeline, serialization layer, search engine, and more. And like all big codebases, each subsystem has its own idioms, gotchas, and “you just have to know” moments.&lt;/p&gt;

&lt;p&gt;I’ve been using Claude Code for backend work on Metabase for a while now. It’s pretty good. But overloads Claude’s context window quickly. Every time Claude needs to understand a subsystem, it explores, greps, and reads files. All of that exploration eats your context window. Even when Claude spawns subagents, they will need to do a lot of extra work to get up to speed on the domain.&lt;/p&gt;

&lt;p&gt;I &lt;a href=&quot;https://gist.github.com/escherize/1cdd92a89cb52a1ce4be1a0cce0467b5&quot;&gt;built some custom subagents&lt;/a&gt; to fix this.&lt;/p&gt;

&lt;h2 id=&quot;what-are-subagents-and-why-did-i-make-ten-of-them&quot;&gt;What are subagents and why did I make ten of them?&lt;/h2&gt;

&lt;p&gt;Metabase’s backend has natural domain boundaries. The query processor is a 68-stage middleware pipeline that compiles MBQL (the Metabase Query Language) to SQL across 18 database dialects. The &lt;a href=&quot;/docs/latest/permissions/start&quot;&gt;permissions&lt;/a&gt; system is a multi-granularity graph that handles &lt;a href=&quot;/docs/latest/permissions/row-and-column-security&quot;&gt;row-level security&lt;/a&gt;, &lt;a href=&quot;/docs/latest/permissions/database-routing&quot;&gt;database routing&lt;/a&gt;, and &lt;a href=&quot;/docs/latest/permissions/impersonation&quot;&gt;connection impersonation&lt;/a&gt;. The notification system renders charts to images inside a JVM. These are different worlds.&lt;/p&gt;

&lt;p&gt;A single generalist Claude session can navigate any of them, but it pays a context tax every time it switches domains. Subagents eliminate that tax by front-loading domain knowledge into the system prompt.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://code.claude.com/docs/en/sub-agents&quot;&gt;Subagents&lt;/a&gt; are a Claude Code feature that lets you define specialized AI assistants as markdown files. They each get their own context window, system prompt, memory, toolkit, and model selection.&lt;/p&gt;

&lt;p&gt;I used Claude to write the “job descriptions” for each agent. I described the domain and what an expert would know, and Claude helped me flesh out the codebase locations, investigation patterns, caveats, and testing strategies. Each agent ended up being roughly 2,000-3,000 tokens worth (about 150 lines of markdown) of dense, useful context that can’t be easily inferred from the code.&lt;/p&gt;

&lt;h2 id=&quot;whats-inside-an-agent-file&quot;&gt;What’s inside an agent file?&lt;/h2&gt;

&lt;p&gt;Each agent is a markdown file that follows the same pattern of, domain knowledge → codebase locations → investigation approach → caveats → testing strategies. It’s a “here’s everything you need to be useful in this corner of the codebase” document.&lt;/p&gt;

&lt;p&gt;Every file starts with YAML frontmatter:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mbql-expert&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Use&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;working&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Metabase&apos;s&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;processor,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;MBQL&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;language,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;SQL&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;compilation...&quot;&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;opus&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;user&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;description&lt;/code&gt; tells Claude &lt;em&gt;when&lt;/em&gt; to delegate. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; picks which Claude model the subagent uses. And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory: user&lt;/code&gt; gives the agent a persistent directory at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude/agent-memory/mbql-expert/&lt;/code&gt; where it records learnings across sessions.&lt;/p&gt;

&lt;p&gt;The body of the file is the actual domain knowledge. Here’s a trimmed look at what the mbql-expert knows:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You are a senior backend engineer with deep expertise
in Metabase&apos;s query processor (QP), MBQL query language,
and the entire query compilation pipeline.

&lt;span class=&quot;gu&quot;&gt;## Your Domain Knowledge&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### The Query Processor Pipeline&lt;/span&gt;
You understand the QP&apos;s ring-style middleware pipeline
with its four phases:
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Around middleware**&lt;/span&gt; (3 layers)
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Preprocessing**&lt;/span&gt; (44 layers) — source card resolution,
  parameter substitution, join resolution, temporal bucketing...
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Execution**&lt;/span&gt; (8 layers) — caching, permissions, result metadata
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Postprocessing**&lt;/span&gt; (13 layers) — formatting, timezone conversion...

&lt;span class=&quot;gu&quot;&gt;### Key Codebase Locations&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`src/metabase/query_processor/`&lt;/span&gt; — QP core
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`src/metabase/driver/sql/`&lt;/span&gt; — SQL driver base
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`modules/drivers/`&lt;/span&gt; — database-specific drivers

&lt;span class=&quot;gu&quot;&gt;### Important Caveats&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Middleware ordering matters. Adding middleware in the wrong
  position causes subtle bugs.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; A fix at the &lt;span class=&quot;sb&quot;&gt;`:sql`&lt;/span&gt; level affects ALL SQL databases.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; BigQuery is not standard SQL. Oracle has no BOOLEAN type.

&lt;span class=&quot;gu&quot;&gt;### REPL-Driven Development&lt;/span&gt;
Use &lt;span class=&quot;sb&quot;&gt;`clj-nrepl-eval`&lt;/span&gt; to evaluate middleware transformations
step by step...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-ten-subagents&quot;&gt;The ten subagents&lt;/h2&gt;

&lt;p&gt;I used Claude to help with defining these specific agents, framed as “job descriptions” like you’d post online (but specific to each section of our code). Our module system and namespace documentation help here, but I reviewed it to make sure it was reasonable.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Agent&lt;/th&gt;
      &lt;th&gt;Domain&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;mbql-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Query processor, MBQL language, SQL compilation, middleware pipeline, HoneySQL, streaming execution&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;permissions-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Access control, sandboxing, SSO (SAML/OIDC/LDAP), connection impersonation, embedding security&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;platform-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;App database, HTTP server, API framework, settings system, migrations, Quartz scheduling&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;enterprise-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Serialization, SCIM provisioning, multi-tenancy, database routing, dependency tracking&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;content-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Collections, dashboards, cards, models, metrics, revisions, parameter mappings&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;notifications-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Dashboard subscriptions, alerts, email/Slack rendering, chart image generation&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;drivers-and-sync&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Database drivers, metadata sync, fingerprinting, type mapping, connection management&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;search-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Search indexing, scoring/ranking, X-ray auto-analysis, semantic search&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;ai-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Metabot v3, LLM tool calling, context engineering, SQL generation&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;transforms-expert&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Data actions, CSV uploads, transform pipeline, workspace management, model persistence&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;I’ve made &lt;a href=&quot;https://gist.github.com/escherize/1cdd92a89cb52a1ce4be1a0cce0467b5&quot;&gt;all ten markdown files available for you to take a look at&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;my-favorite-mbql-expert&quot;&gt;My Favorite: mbql-expert&lt;/h2&gt;

&lt;p&gt;The query processor is the heart of Metabase and the hardest thing to navigate. It’s a 68-stage middleware pipeline where a query enters as MBQL, gets rewritten 44 times during preprocessing, compiled to SQL via HoneySQL, executed, and then post-processed through 13 more stages. Oh, and some middleware runs &lt;em&gt;twice&lt;/em&gt; because later stages can introduce structure that earlier stages need to process again. Query processing is a complex problem, especially when dealing with many different databases.&lt;/p&gt;

&lt;p&gt;The mbql-expert already knows all of this. When I say “trace why this nested query with joins produces wrong results on Redshift,” it doesn’t start by grepping. It reasons about which middleware stages touch join aliases, checks Redshift-specific driver overrides, and examines the HoneySQL output. That’s the difference between a generalist exploring and a specialist investigating.&lt;/p&gt;

&lt;h2 id=&quot;how-i-actually-use-the-agents&quot;&gt;How I actually use the agents&lt;/h2&gt;

&lt;p&gt;The nice thing is you don’t need special syntax. Just mention the agent:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“Bounce this off the enterprise expert — will this serialization change break round-trip import/export?”&lt;/li&gt;
  &lt;li&gt;“Ask the permissions expert how row-and-column security interacts with joined tables.”&lt;/li&gt;
  &lt;li&gt;“Have the mbql expert review this HoneySQL compilation change.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude reads the intent and delegates work to the right agent. If you want to be explicit, you can also @-mention agents directly.&lt;/p&gt;

&lt;p&gt;One useful pattern for explicit mentions: &lt;strong&gt;launching multiple agents in parallel&lt;/strong&gt;. When reviewing a change that touches the query processor and permissions, I’ll ask Claude to have both experts weigh in simultaneously. Each expert investigates in its own context, and the results come back without cross-contaminating each other’s exploration.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;tips-for-making-your-own-subagents&quot;&gt;Tips for making your own subagents&lt;/h2&gt;

&lt;p&gt;The pattern I’ve described works for any large codebase with distinct subsystems. See &lt;a href=&quot;https://code.claude.com/docs/en/sub-agents#quickstart-create-your-first-subagent&quot;&gt;Claude’s subagent documentation&lt;/a&gt; for the details on how to structure the files.&lt;/p&gt;

&lt;p&gt;Here are a few things that worked for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Have Claude help you write the agents: describe the domain and what an expert would know, and iterate on the system prompt together. (That’s how I built these.)&lt;/li&gt;
  &lt;li&gt;Spend more time iterating on the description than the actual system prompts. The 2-3 sentence description in the frontmatter of each Markdown file is what Claude reads to decide when to delegate. A description that says “use for query processor work” is too vague, Claude won’t reliably match it. You want specific trigger words: “MBQL query language, SQL compilation, middleware pipeline, HoneySQL, streaming execution.” Think of it as writing a routing rule, not a job title.&lt;/li&gt;
  &lt;li&gt;I include codebase locations in every agent, but the most durable content is the investigation patterns and caveats. Directories get renamed but the fact that “some middleware runs twice because later stages introduce structure that earlier stages need to re-process” isn’t going away anytime soon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personal agents live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude/agents/&lt;/code&gt;, and project-local agents go in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/agents/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now go build some agents!&lt;/p&gt;

&lt;h2 id=&quot;more-from-metabase-engineering&quot;&gt;More from Metabase Engineering&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/blog/reprobot-github-issue-triage-agent&quot;&gt;Meet Repro-Bot, our GitHub issue triage agent&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/blog/lessons-learned-building-ai-analytics-agents&quot;&gt;Lessons learned from building AI analytics agents: build for chaos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/ten-custom-subagents</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/ten-custom-subagents</guid>
        
        
        <category>Engineering</category>
        
      </item>
    
      <item>
        <title>Metabase AI Hackathon</title>
        <description>&lt;p&gt;Show us what you can build with the new AI tools and get the chance to win a Metabase mechanical keyboard.&lt;/p&gt;

&lt;p&gt;Metabase now ships an MCP server, an Agent API, and Metabot in Slack: three different ways to plug your analytics into whatever agent or workflow you’re building. All in open source, which means the tools are yours, the semantic layer is yours, what you build on top of them is up to you. So we figured: let’s see what the community can build.&lt;/p&gt;

&lt;h2 id=&quot;join-the-hackathon&quot;&gt;Join the hackathon&lt;/h2&gt;

&lt;p&gt;To enter:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Build something&lt;/strong&gt; using the &lt;a href=&quot;/blog/metabase-ai-hackathon#the-ai-tools-to-use&quot;&gt;AI tools listed below&lt;/a&gt;. Solo or with a team.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Write it up&lt;/strong&gt; — a social post, blog post, short video,  or repo with a good README. Something that shows off what’ve built.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Submit your entry by sharing it&lt;/strong&gt; on X or LinkedIn, tag &lt;a href=&quot;https://x.com/metabase&quot;&gt;@metabase&lt;/a&gt;, and use &lt;strong&gt;#MetabaseAI&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Submit by Tuesday 05/05/2026&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Build whatever you like. Analytics agents, dashboards that summarize themselves, something weird we didn’t think of — all fair game. If it uses one of our AI tools somewhere in the loop, it counts.&lt;/p&gt;

&lt;div class=&quot;blog-callout p2 mt3 mb4 bordered rounded&quot; style=&quot;font-family: Lato, sans-serif;&quot;&gt;
Get inspired by this demo of a voice-powered agent built with the Metabase agent API. Ask a question out loud, get an answer grounded in your actual data.

  &lt;a href=&quot;https://www.youtube.com/watch?v=wiOBbgustQg&quot;&gt;Watch the full demo&lt;/a&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-ai-tools-to-use&quot;&gt;The AI tools to use&lt;/h2&gt;

&lt;p&gt;Your submission must use at least one of the following tools:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/ai/mcp&quot;&gt;Metabase MCP server&lt;/a&gt;&lt;/strong&gt; — plug Metabase into Claude, Cursor, or any MCP client. Ask your data questions from your editor.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/ai/agent-api&quot;&gt;Agent API&lt;/a&gt;&lt;/strong&gt; — build your own agent on top of your Metabase’s semantic layer.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/ai/file-based-development&quot;&gt;File-based development&lt;/a&gt;&lt;/strong&gt; - let an AI agent create and edit Metabase content serialized as YAML files.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/ai/metabot-slack&quot;&gt;Metabot in Slack&lt;/a&gt;&lt;/strong&gt; — point Metabot at a Slack channel and let your team query data where they already argue about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All AI features are available for everyone with Metabase 60. If you’re on an earlier version, please &lt;a href=&quot;/docs/latest/installation-and-operation/upgrading-metabase&quot;&gt;upgrade&lt;/a&gt;. If you’re on Cloud, we’re rolling 60 out soon (email help@metabase.com if you want to upgrade now).&lt;/p&gt;

&lt;div style=&quot;width: ; margin-bottom: 2em;&quot;&gt;
    
        &lt;div class=&quot;youtube-container&quot; style=&quot;position: relative;
                    width: 100%;
                    height: 0;
                    padding-bottom: 56.25%&quot;&gt;

          &lt;iframe class=&quot;youtube-video&quot; style=&quot;position: absolute;
                         top: 0;
                         left: 0;
                         width: 100%;
                         height: 100%;
                         border: 0&quot; allowfullscreen=&quot;&quot; src=&quot;https://www.youtube.com/embed/XfsAE8Ojqxg&quot;&gt;
	  &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;h2 id=&quot;prizes&quot;&gt;Prizes&lt;/h2&gt;

&lt;p&gt;We’re picking &lt;strong&gt;two winners&lt;/strong&gt;. Each one gets:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A &lt;strong&gt;Metabase mechanical keyboard&lt;/strong&gt; (the good kind, not the kind that clacks at you judgmentally)&lt;/li&gt;
  &lt;li&gt;A feature on our blog&lt;/li&gt;
  &lt;li&gt;A shout-out across our socials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/metabase-mechanical-keyboard.png&quot; alt=&quot;Metabase mechanical keyboard&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Hackathon opens: Tuesday, April 21st&lt;/li&gt;
  &lt;li&gt;Submissions close: Tuesday, May 5th&lt;/li&gt;
  &lt;li&gt;Winners announced: Thursday, May 7th&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have fun!&lt;/p&gt;
</description>
        <pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/metabase-ai-hackathon</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/metabase-ai-hackathon</guid>
        
        
        <category>News</category>
        
      </item>
    
      <item>
        <title>Meet Repro-Bot, our GitHub issue triage agent</title>
        <description>&lt;p&gt;Reproducing bug reports is one of the most time-consuming parts of maintaining an open source project. We built an AI agent called Repro-Bot to help us with this task. In this post, we’re sharing how we built it and show you how you can build your own. If you want to skip ahead and check out our code, &lt;a href=&quot;https://github.com/metabase/repro-bot&quot;&gt;take a look at the Repro-Bot repo&lt;/a&gt;!&lt;/p&gt;

&lt;h2 id=&quot;repro-bot-automates-the-boring-parts&quot;&gt;Repro-Bot automates the boring parts&lt;/h2&gt;

&lt;p&gt;Think about how you, a human person, would reproduce a bug report:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Set up an environment identical (or at least similar) to what the reporter has.&lt;/li&gt;
  &lt;li&gt;Follow the steps provided in the issue.&lt;/li&gt;
  &lt;li&gt;If you can reproduce the bug:
    &lt;ul&gt;
      &lt;li&gt;Write a test for the bug&lt;/li&gt;
      &lt;li&gt;Fix it&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;If you can’t reproduce, think through why:
    &lt;ul&gt;
      &lt;li&gt;Is there information missing?&lt;/li&gt;
      &lt;li&gt;Are there any hidden dependencies?&lt;/li&gt;
      &lt;li&gt;Did anything change recently?&lt;/li&gt;
      &lt;li&gt;etc&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process is a mix of judgement calls (like ”fix it,” or what constitutes a relevant change), and chores like setting up the environment, following the steps, and asking the same questions over and over.&lt;/p&gt;

&lt;p&gt;Repro-Bot automates the boring parts and gets us started on fixing the issue.&lt;/p&gt;

&lt;h3 id=&quot;results-repro-steps-findings-possible-root-cause&quot;&gt;Results: repro steps, findings, possible root cause&lt;/h3&gt;

&lt;p&gt;As Repro-Bot attempts to repro an issue, it generates a report with its findings, a pointer to where in the code the bug probably occurs, etc.. For example, here is the first part of its output for &lt;a href=&quot;https://github.com/metabase/metabase/issues/68802&quot;&gt;this issue about disappearing percentages on some pie charts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../images/posts/engineering-2026/reprobot-repro.png&quot; alt=&quot;Repro-Bot reproducing an issue with a summary and reproduction steps&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This information helps us respond to the person who reported the issue faster and get more details from them while it’s still fresh in their mind. We’ve also cleared out a number of issues from our backlog that Repro-Bot confirmed we had already fixed.&lt;/p&gt;

&lt;p&gt;Of course, Repro-Bot isn’t infallible. Sometimes it can’t repro an issue. Sometimes it thinks it has reproduced a bug when it hasn’t. But even in those cases, Repro-Bot’s reports are still valuable. They give us hints and chronicle dead-ends, both of which save devs time getting to the root cause.&lt;/p&gt;

&lt;h3 id=&quot;how-repro-bot-works-and-how-to-build-your-own&quot;&gt;How Repro-Bot works, and how to build your own&lt;/h3&gt;

&lt;p&gt;The details of Repro-Bot are quite specific to Metabase, but we’ll walk you through its inner workings so you can build a similar agent for your own codebase and development setup. You can also &lt;a href=&quot;https://github.com/metabase/repro-bot&quot;&gt;fork our repo&lt;/a&gt; and adapt it to your workflow.&lt;/p&gt;

&lt;p&gt;Repro-Bot needs to be able to perform these tasks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Parse and understand issues&lt;/li&gt;
  &lt;li&gt;Spin up a test environment&lt;/li&gt;
  &lt;li&gt;Work through reproduction steps&lt;/li&gt;
  &lt;li&gt;Write tests&lt;/li&gt;
  &lt;li&gt;Write a report&lt;/li&gt;
  &lt;li&gt;Clean up and self-review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s how we approached each one.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/commands/repro.md#phase-2-issue-parsing&quot;&gt;Parsing issues&lt;/a&gt; tells the LLM what you, a developer, are looking for when you analyze the issue yourself. For example, for Metabase, we need information about the Metabase version, the application database, the data warehouse. We also triage the issue as backend-focused or frontend-focussed to guide which tools should the agent use for reproduction.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/skills/playwright-cli/SKILL.md&quot;&gt;Spinning up a test environment&lt;/a&gt; is very specific to your codebase and tools, of course. At Metabase, we use Playwright for browser automation, filling forms, and taking screenshots. Repro-Bot spins up an environment in the same way that a developer would, and uses REPL access to the instance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/commands/repro.md#phase-4-reproduction-max-3-attempts&quot;&gt;To work through the reproduction steps&lt;/a&gt;, we &lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/skills/metabase-reference/SKILL.md&quot;&gt;wrote a skill&lt;/a&gt; that gives code recipes for common actions we see in reported reproduction steps, like inspecting a table or creating a query. We also wrote down some of the “folklore” domain knowledge around some features (like for example that there are two different ways to make a pivot table with two different code paths). Repro-Bot uses the reproduction steps that it previously extracted from the issue, invokes the tools based on the “triage” into backend/frontend, and uses the recipes for those tools to run through the repro steps.
It then evaluates what was tested and if its results match the reported behavior. If the agent can’t determine whether the issue was reproduced, it tries again (but no more than three times total).&lt;/li&gt;
  &lt;li&gt;If the agent reproduces the issue, it &lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/skills/write-test/SKILL.md&quot;&gt;writes a failing test&lt;/a&gt; so that a developer can have something to test against when they’re fixing the issue. We give some directions on the kind of tests we write for our code and troubleshooting common issues, but since the agent already has full access to the codebase, it can learn a lot from just analyzing existing tests.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/commands/repro.md#phase-6-report--post&quot;&gt;Write a report and post to Linear&lt;/a&gt;. This provides the bot with a detailed outline for the report (as you saw in the previous section), as well as instructions for how to post to Linear, and how to prevent the internal report from getting synced back to GitHub.&lt;/li&gt;
  &lt;li&gt;Finally, &lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/commands/repro.md#phase-7-cleanup&quot;&gt;cleanup and self-review&lt;/a&gt;. After each run, Repro-Bot &lt;a href=&quot;https://github.com/metabase/repro-bot/blob/main/.claude/commands/auto-improve.md&quot;&gt;reviews the notes from this and previous runs&lt;/a&gt; to make concrete suggestions for improvements, for example to address tool errors, close any knowledge gaps it found, document new tools it might need, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;integrating-repro-bot-into-the-workflow&quot;&gt;Integrating Repro-Bot into the workflow&lt;/h2&gt;

&lt;p&gt;We use GitHub to collect reported bugs, and Linear to manage development work. To run Repro-Bot, a human tags a bug on GitHub with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.Run Repro-Bot&lt;/code&gt;, which triggers a GitHub action that runs the workflow described above.&lt;/p&gt;

&lt;p&gt;Running the bot is not an automated task by design: a human in the loop is essential to prevent injection attacks. Most issues come from our public GitHub issues, so it would be trivial for somebody to poison context. To guard against this, we sandbox the agent and limit its permissions. We also require a human to review issues before running it to make sure there is nothing suspicious in the issue.&lt;/p&gt;

&lt;p&gt;We intentionally did not ask Repro-Bot to fix the issue. We had initially wanted to make a more end-to-end bot that could do it all, but that wider scope opened up a number of wrong paths the bot could go down. Keeping the agent’s purview limited keeps its output manageable, and we can always introduce more automation downstream.&lt;/p&gt;

&lt;h2 id=&quot;what-we-learned&quot;&gt;What we learned&lt;/h2&gt;

&lt;p&gt;We think that Repro-Bot is an interesting approach to using LLMs and AI tooling for software development, because it’s not about code generation. Our Repro-Bot repo is very specific to our setup, but with the code as a starting point and the description above, you can build your own.&lt;/p&gt;

&lt;p&gt;Repro-Bot has become part of our daily development work, and continues to save us time. We hope that it inspires others to build (and share!) similar tools for themselves.&lt;/p&gt;
</description>
        <pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/reprobot-github-issue-triage-agent</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/reprobot-github-issue-triage-agent</guid>
        
        
        <category>Engineering</category>
        
      </item>
    
      <item>
        <title>Meet Data Studio: tools to curate your semantic layer in Metabase</title>
        <description>&lt;p&gt;Metabase has grown a lot over the past few years. We’ve added a bunch of tools to help people stay on top of their analytics as things scale.&lt;/p&gt;

&lt;p&gt;Eventually, it became clear these tools needed their own home.&lt;/p&gt;

&lt;p&gt;Today, we’re introducing &lt;a href=&quot;/product/data-studio/&quot;&gt;Metabase Data Studio&lt;/a&gt;, a place where teams can shape their data and define shared metrics.&lt;/p&gt;

&lt;h2 id=&quot;analytics-starts-simple-then-it-gets-less-simple&quot;&gt;Analytics starts simple. Then it gets… less simple&lt;/h2&gt;

&lt;p&gt;One goal at Metabase has always been to help non-technical people answer questions with data. And at first, that’s easy. You connect Metabase to your database, build a few dashboards, and things feel straightforward. But over time, cracks in the data model inevitably start to show.&lt;/p&gt;

&lt;p&gt;People aren’t sure which tables to use, dashboard loading times get annoying, and there are three different queries that say “ARR”. AIs don’t stand a chance sifting through this stuff.&lt;/p&gt;

&lt;h2 id=&quot;data-studio-has-all-the-tools-you-need-to-clean-up-the-mess&quot;&gt;Data Studio has all the tools you need to clean up the mess&lt;/h2&gt;

&lt;div style=&quot;width: ; margin-bottom: 2em;&quot;&gt;
    
        &lt;div class=&quot;youtube-container&quot; style=&quot;position: relative;
                    width: 100%;
                    height: 0;
                    padding-bottom: 56.25%&quot;&gt;

          &lt;iframe class=&quot;youtube-video&quot; style=&quot;position: absolute;
                         top: 0;
                         left: 0;
                         width: 100%;
                         height: 100%;
                         border: 0&quot; allowfullscreen=&quot;&quot; src=&quot;https://www.youtube.com/embed/ac3zH1SyT70&quot;&gt;
	  &lt;/iframe&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;p&gt;Data Studio lets teams transform raw tables into analytics-ready datasets. You can define reusable metrics (like MRR) and segments (like Active Customers) that everyone (including AI!) can trust when building dashboards and questions.&lt;/p&gt;

&lt;p&gt;Data Studio lives in Metabase: no extra tools, no duplicate work, no workflow overhauls, just publish and share instantly. You can start small and grow into it naturally as analytics becomes more shared and harder to change.&lt;/p&gt;

&lt;h2 id=&quot;the-tools-in-the-toolbox&quot;&gt;The tools in the toolbox&lt;/h2&gt;

&lt;p&gt;The first version of Data Studio ships with the following tools:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/data-studio/library&quot;&gt;Library&lt;/a&gt;&lt;/strong&gt;: A curated space for your organization’s most trusted analytics content—tables, metrics, and SQL snippets that your data team recommends.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/data-studio/data-structure&quot;&gt;Data structure&lt;/a&gt;&lt;/strong&gt;: Add table metadata to make tables easier to work with.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/exploration-and-organization/data-model-reference#glossary&quot;&gt;Glossary&lt;/a&gt;&lt;/strong&gt;: Define terms relevant to your business, both for people and agents trying to understand your data.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/data-studio/dependency-graph&quot;&gt;Dependency graph&lt;/a&gt;&lt;/strong&gt;: A visual map of how your content connects, so you can understand the impact of changes before you make them.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/data-studio/dependency-diagnostics&quot;&gt;Dependency diagnostics&lt;/a&gt;&lt;/strong&gt;: See which items have broken dependencies, or that aren’t used.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/data-studio/transforms/transforms-overview&quot;&gt;Transforms&lt;/a&gt;&lt;/strong&gt;: Wrangle your data in Metabase, write the query results back to your database, and reuse them in Metabase as sources for new queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And we have more to come, so stay tuned.&lt;/p&gt;

&lt;h2 id=&quot;open-source-at-the-core&quot;&gt;Open source at the core&lt;/h2&gt;

&lt;p&gt;We want data structure and curation to be accessible to everyone, which is why foundational features of Data Studio are available in our open source edition, with Pro and Enterprise features to grow into as you need them.&lt;/p&gt;

&lt;h2 id=&quot;do-i-even-need-to-care-about-data-studio&quot;&gt;Do I even need to care about Data Studio?&lt;/h2&gt;

&lt;p&gt;People tend to need some kind of data transformations when they have multiple sources of data (like your application and payments data), or a bunch of normalized tables. If you’re under 50 tables in your schema, don’t stress, watercress. If you have multiple data sources or a lot of tables, chances are you’ve been paying a tax on clarity, correctness, and performance. Data Studio can help get you sorted.&lt;/p&gt;

&lt;div class=&quot;blog-callout p2 mb4 bordered rounded&quot;&gt;
Data Studio is just one part of a bumper release. &lt;a href=&quot;/releases/metabase-59&quot;&gt;Check out what else is new in v59&lt;/a&gt;
&lt;/div&gt;

&lt;h2 id=&quot;how-to-get-started-with-data-studio&quot;&gt;How to get started with Data Studio&lt;/h2&gt;

&lt;p&gt;Data Studio ships with both OSS and EE editions (&lt;a href=&quot;/pricing&quot;&gt;with some paid features&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Admins can find Data Studio from the top right grid icon. Some paid plans can grant non-admins access to Data Studio by adding people to the Data Analysts group.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://store.metabase.com/checkout&quot;&gt;Try Metabase Pro for free&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/meet-data-studio-semantic-layer</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/meet-data-studio-semantic-layer</guid>
        
        
        <category>Analytics</category>
        
        <category>and</category>
        
        <category>BI</category>
        
      </item>
    
      <item>
        <title>February 2026 vulnerability: What happened?</title>
        <description>&lt;h2 id=&quot;what-happened&quot;&gt;What happened?&lt;/h2&gt;

&lt;p&gt;Sho Odagiri, a security researcher, &lt;a href=&quot;https://github.com/metabase/metabase/security/advisories/GHSA-vcj8-rcm8-gfj9&quot;&gt;reported a vulnerability&lt;/a&gt; in Metabase’s notification API. The vulnerability allowed an authenticated user to craft a specially formatted notification template that could extract database connection details, including credentials, and send them via outbound email.&lt;/p&gt;

&lt;h2 id=&quot;who-was-affected&quot;&gt;Who was affected?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We have no evidence that this vulnerability was exploited by any customer or malicious actor prior to the fix being released.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;See the &lt;strong&gt;Fixed versions&lt;/strong&gt; below, and find the latest point version for the Metabase version you’re running. If you’re running a point version &lt;em&gt;below&lt;/em&gt; that version, you’re still vulnerable and should upgrade immediately.&lt;/p&gt;

&lt;h2 id=&quot;why-did-it-happen&quot;&gt;Why did it happen?&lt;/h2&gt;

&lt;p&gt;Two independent changes introduced this vulnerability:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;We updated the notification system to support user-supplied Handlebars templates for rendering email content.&lt;/li&gt;
  &lt;li&gt;We added metadata objects to query results, which could be traversed to access database connection details.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, these changes made it possible for an authenticated user to write a template that could extract sensitive database details via an outgoing email.&lt;/p&gt;

&lt;p&gt;Part of what made this vulnerability difficult to catch is that the Handlebars library did not clearly document a method resolver that allows templates to invoke arbitrary Java methods.&lt;/p&gt;

&lt;h2 id=&quot;what-did-we-fix&quot;&gt;What did we fix?&lt;/h2&gt;

&lt;p&gt;We addressed this vulnerability through two fixes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Locked down the Handlebars template engine.&lt;/strong&gt; We removed the method resolver from the Handlebars library configuration, which prevents templates from invoking arbitrary Java methods on objects in the rendering context. This eliminates the ability for user-supplied templates to traverse into internal objects.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stripped metadata from query results used in notifications.&lt;/strong&gt; We ensured that internal metadata objects—which previously could carry a reference to database connection details—are no longer present in the notification rendering context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;fixed-versions&quot;&gt;Fixed versions&lt;/h2&gt;

&lt;p&gt;All Metabase Cloud instances have been upgraded and are no longer vulnerable.&lt;/p&gt;

&lt;p&gt;If you are self-hosted and haven’t already upgraded, please upgrade to one of the following versions (or higher) for your respective version.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Version 55: &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.55.20&quot;&gt;v0.55.20&lt;/a&gt; / &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.55.20&quot;&gt;v1.55.20&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Version 56: &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.56.20&quot;&gt;v0.56.20&lt;/a&gt; / &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.56.20&quot;&gt;v1.56.20&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Version 57: &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.57.13&quot;&gt;v0.57.13&lt;/a&gt; / &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.57.13&quot;&gt;v1.57.13&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Version 58: &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.58.7&quot;&gt;v0.58.7&lt;/a&gt; / &lt;a href=&quot;https://github.com/metabase/metabase/releases/tag/v0.58.7&quot;&gt;v1.58.7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-are-we-doing-to-prevent-this-in-the-future&quot;&gt;What are we doing to prevent this in the future?&lt;/h2&gt;

&lt;p&gt;Along with the fixes we’ve made, we’re working through additional improvements to reduce risk:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Improving logging around template rendering&lt;/strong&gt; so that we can audit user-supplied templates and detect unusual behavior.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Adding a wrapper around database credential access&lt;/strong&gt; to prevent credential access outside of designated connection establishment paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Patches are live across all affected versions, and we have no evidence this vulnerability was exploited before the fix landed. We’re tightening template evaluation, locking down credential access paths, and improving logging to catch unusual behavior early.&lt;/p&gt;

&lt;p&gt;If you’re self-hosted and haven’t upgraded already, please upgrade as soon as possible.&lt;/p&gt;

&lt;h2 id=&quot;credits&quot;&gt;Credits&lt;/h2&gt;

&lt;p&gt;Hat tip to Sho Odagiri from GMO Cybersecurity by Ierae, Inc for discovering and disclosing this vulnerability.&lt;/p&gt;

&lt;h2 id=&quot;questions-or-concerns&quot;&gt;Questions or concerns?&lt;/h2&gt;

&lt;p&gt;Reach out at &lt;a href=&quot;mailto:support@metabase.com&quot;&gt;support@metabase.com&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Mon, 02 Mar 2026 12:10:18 +0000</pubDate>
        <link>https://www.metabase.com/blog/security-vulnerability-postmortem</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/security-vulnerability-postmortem</guid>
        
        
        <category>News</category>
        
      </item>
    
      <item>
        <title>Security update available for Metabase - Please upgrade now</title>
        <description>&lt;p&gt;An independent security researcher Sho Odagiri from GMO Cybersecurity by Ierae submitted a severe issue with Metabase. We generally don’t blog about every bug, but this one is dangerous so we want to make sure that we reach out on all channels to our community to let them know that they should pay attention to this.&lt;/p&gt;

&lt;p&gt;While we have no evidence that the vulnerability was ever exploited in the wild, and exploiting this vulnerability isn’t simple, &lt;strong&gt;if you are self-hosting Metabase, you should IMMEDIATELY update your Metabase instances (if you have not already).&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-vulnerability&quot;&gt;The vulnerability&lt;/h2&gt;

&lt;p&gt;The vulnerability allows an authenticated user (including embedding users) to retrieve sensitive information from a Metabase instance, including database access credentials. For more info, check out the &lt;a href=&quot;https://github.com/metabase/metabase/security/advisories/GHSA-vcj8-rcm8-gfj9&quot;&gt;security advisory&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;are-you-affected&quot;&gt;Are you affected?&lt;/h2&gt;

&lt;h3 id=&quot;metabase-cloud-customers-dont-need-to-upgrade&quot;&gt;Metabase Cloud customers don’t need to upgrade&lt;/h3&gt;

&lt;p&gt;No action needed. We’ve already upgraded your Metabase, and you’re no longer vulnerable.&lt;/p&gt;

&lt;h3 id=&quot;all-self-hosted-metabases-including-customers-should-upgrade-immediately&quot;&gt;All self-hosted Metabases, including customers, should upgrade immediately&lt;/h3&gt;

&lt;p&gt;IF you haven’t already, you should immediately upgrade to the latest point version of whichever Metabase version you’re running.&lt;/p&gt;

&lt;p&gt;See the &lt;a href=&quot;#minimum-safe-releases-for-each-metabase-version&quot;&gt;list of minimum safe releases&lt;/a&gt; below, and find the latest point version for the Metabase version you’re running. If you’re running a point version &lt;em&gt;below&lt;/em&gt; that version, you’re still vulnerable and should upgrade immediately.&lt;/p&gt;

&lt;p&gt;For example, if you are running 1.58.6, you should upgrade to 1.58.7 release or later. If you’re running a version of Metabase below version 55, you should upgrade to one of the versions listed below. You can find your current version by clicking on the “gear” icon in the upper right and selecting “About Metabase.”&lt;/p&gt;

&lt;h3 id=&quot;if-youre-running-a-custom-fork-of-metabase-reach-out-to-us-for-the-patches&quot;&gt;If you’re running a custom fork of Metabase, reach out to us for the patches&lt;/h3&gt;

&lt;p&gt;Email us at help@metabase.com so we can provide you the appropriate patches.&lt;/p&gt;

&lt;h2 id=&quot;minimum-safe-releases-for-each-metabase-version&quot;&gt;Minimum safe releases for each Metabase version&lt;/h2&gt;

&lt;p&gt;The downloads below include the minimum safe release for each Metabase version.&lt;/p&gt;

&lt;h3 id=&quot;55&quot;&gt;55&lt;/h3&gt;

&lt;p&gt;v0.55.20&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase:v0.55.20&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/v0.55.20/metabase.jar&quot;&gt;https://downloads.metabase.com/v0.55.20/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.55.20&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase-enterprise:v1.55.20&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/enterprise/v1.55.20/metabase.jar&quot;&gt;https://downloads.metabase.com/enterprise/v1.55.20/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;56&quot;&gt;56&lt;/h3&gt;

&lt;p&gt;v0.56.20&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase:v0.56.20&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/v0.56.20/metabase.jar&quot;&gt;https://downloads.metabase.com/v0.56.20/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.56.20&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase-enterprise:v1.56.20&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/enterprise/v1.56.20/metabase.jar&quot;&gt;https://downloads.metabase.com/enterprise/v1.56.20/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;57&quot;&gt;57&lt;/h3&gt;

&lt;p&gt;v0.57.13&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase:v0.57.13&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/v0.57.13/metabase.jar&quot;&gt;https://downloads.metabase.com/v0.57.13/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.57.13&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase-enterprise:v1.57.13&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/enterprise/v1.57.13/metabase.jar&quot;&gt;https://downloads.metabase.com/enterprise/v1.57.13/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;58&quot;&gt;58&lt;/h3&gt;

&lt;p&gt;v0.58.7&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase/v0.58.7&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/v0.58.7/metabase.jar&quot;&gt;https://downloads.metabase.com/v0.58.7/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.58.7&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker image: metabase/metabase-enterprise/v1.58.7&lt;/li&gt;
  &lt;li&gt;Download the JAR here: &lt;a href=&quot;https://downloads.metabase.com/enterprise/v1.58.7/metabase.jar&quot;&gt;https://downloads.metabase.com/enterprise/v1.58.7/metabase.jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;credits&quot;&gt;Credits&lt;/h2&gt;

&lt;p&gt;We thank Sho Odagiri from GMO Cybersecurity by Ierae, Inc for discovering and disclosing this vulnerability.&lt;/p&gt;
</description>
        <pubDate>Thu, 19 Feb 2026 12:10:18 +0000</pubDate>
        <link>https://www.metabase.com/blog/security-vulnerability</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/security-vulnerability</guid>
        
        
        <category>News</category>
        
      </item>
    
      <item>
        <title>Lessons learned from building AI analytics agents: build for chaos</title>
        <description>&lt;p&gt;Last year, I came back from a conference, pulled the latest code, and fired up our brand new version of &lt;a href=&quot;/features/metabase-ai&quot;&gt;Metabot&lt;/a&gt; to show our CEO the progress we’d made. While I was away, the team had been shipping new features and improvements.&lt;/p&gt;

&lt;p&gt;My excitement quickly turned into one of the most embarrassing moments of my professional career. Metabot had transformed into a confused intern: eager to help but unable to remember what tools it had or how to use them.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../images/posts/metabot-header.png&quot; alt=&quot;A broken Metabot on fire, and an embarrassed engineer&quot; /&gt;&lt;/p&gt;

&lt;p&gt;But here’s the thing: this disaster taught us a lot about building production AI agents. In this post, I’ll walk you through what actually broke, why it happened, and the patterns we discovered that actually work in production for us.&lt;/p&gt;

&lt;p&gt;If you want the full story with all the details, you can watch &lt;a href=&quot;https://www.youtube.com/watch?v=EnvozxnWjP4&quot;&gt;the talk we gave at the AI Engineering conference 2025 in Paris&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-we-were-building-and-why-its-hard&quot;&gt;What we were building (and why it’s hard)&lt;/h2&gt;

&lt;p&gt;When we started building &lt;a href=&quot;/features/metabase-ai&quot;&gt;Metabot&lt;/a&gt;, the text-to-SQL space was already crowded: you describe what you want, give an LLM your database schema, and it generates SQL. Easy, right?&lt;/p&gt;

&lt;p&gt;Yes and no.&lt;/p&gt;

&lt;p&gt;The happy path works great - you’ve probably seen the demos with 5 well-documented tables and simple questions. But even if it works, there’s a problem: &lt;strong&gt;not everyone speaks SQL&lt;/strong&gt;. A query can look fancy and return results, but how do you know if it’s answering the right question?&lt;/p&gt;

&lt;p&gt;We wanted to go beyond SQL generation. Our goal was to leverage the &lt;a href=&quot;/features/query-builder&quot;&gt;Metabase query builder&lt;/a&gt;, a visual interface where users can click together queries and actually see what filters and aggregations are applied. This gives non-SQL users a way to validate and iterate on results themselves more easily.&lt;/p&gt;

&lt;p&gt;But here’s where it gets hard:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SQL is baked into LLM training data. Our query builder language? Not so much.&lt;/li&gt;
  &lt;li&gt;Real customer data is messy: hundreds of tables, vague descriptions, legacy cruft.&lt;/li&gt;
  &lt;li&gt;Humans are notoriously bad at providing context: “How many customers did we lose?” (Which time period? What’s a “customer”? Logo churn or revenue churn?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real challenge wasn’t query generation. It was building an agent that could navigate this chaos by understanding what users are looking at, what they actually mean, and helping them find answers even when they don’t know how to ask the question.&lt;/p&gt;

&lt;p&gt;That’s what Metabot set out to do. And that’s what spectacularly broke.&lt;/p&gt;

&lt;h2 id=&quot;what-broke-local-optimization&quot;&gt;What broke: local optimization&lt;/h2&gt;

&lt;p&gt;The demo failure traced back to parallel development without integration testing. One engineer perfected the context awareness to make sure Metabot knew exactly what dashboard you were looking at. Another engineer optimized the querying tool, fine-tuning descriptions, parameters, and prompts until it worked beautifully in isolation.&lt;/p&gt;

&lt;p&gt;Together, they created chaos.&lt;/p&gt;

&lt;p&gt;The LLM doesn’t experience your architecture. It sees one context window: every instruction, every tool description, every piece of dynamic state, flattened into a single prompt. Our individually-optimized components were sending contradictory signals. Tool descriptions assumed different conventions. Instructions overlapped and conflicted.
The model couldn’t figure out what we wanted because we were telling it multiple inconsistent things simultaneously.&lt;/p&gt;

&lt;p&gt;The fix required thinking differently about what we were building. We weren’t building a querying tool with some context features. &lt;strong&gt;We were building a context engineering system&lt;/strong&gt;. The LLM handles the generation; our job is to ensure it sees clean, unambiguous context at every decision point.&lt;/p&gt;

&lt;h2 id=&quot;what-worked-context-engineering-over-prompt-engineering&quot;&gt;What worked: context engineering over prompt engineering&lt;/h2&gt;

&lt;p&gt;We stopped front-loading prompts and started engineering context throughout the agent’s lifecycle. Three patterns—optimized data representations, just-in-time instructions, and actionable error guidance—transformed how the LLM understood and used its tools&lt;/p&gt;

&lt;h3 id=&quot;llm-optimized-data-representations&quot;&gt;LLM-optimized data representations&lt;/h3&gt;

&lt;p&gt;We stopped dumping raw API responses into the context. Instead, we built explicit serialization templates for every data object Metabot works with — tables, fields, dashboards, questions — optimized for LLM consumption:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;table&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;database_id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ### Description  ### Fields | Field Name | Field ID |
  Type | Description | |------------|----------|------|-------------| 
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This structured format gives the LLM consistent, hierarchical context it can parse reliably. Table metadata, field types, and relationships are always in the same place, reducing hallucination and tool misuse. The format is also reusable across all of Metabot’s tools, so when one person optimizes it, everyone benefits.&lt;/p&gt;

&lt;p&gt;Use this pattern when your agent works with complex domain objects that appear across multiple tools or conversation turns.&lt;/p&gt;

&lt;h3 id=&quot;just-in-time-instructions&quot;&gt;Just-in-time instructions&lt;/h3&gt;

&lt;p&gt;Our original architecture front-loaded everything into the system prompt. The LLM ignored most of it.&lt;/p&gt;

&lt;p&gt;So we tried something different: include instructions in tool results, right in the relevant moment:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Chart created with ID 123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;instructions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;Chart created but not yet visible to the user.

  To show them:
  - Navigate: use navigate_to tool with chart_id 123
  - Reference: include [View chart](metabase://chart/123) in your response
  &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When a chart gets created, tell the LLM &lt;em&gt;right then&lt;/em&gt; how to show it. The LLM pays attention to context that shows up exactly when it’s relevant, not buried in a system prompt from 20 messages ago.&lt;/p&gt;

&lt;h3 id=&quot;explicit-error-guidance&quot;&gt;Explicit error guidance&lt;/h3&gt;

&lt;p&gt;This pattern is more commonly known, but worth emphasizing: don’t just return error messages, return recovery paths instead.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Table &apos;orders_v2&apos; not found&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;guidance&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;This table may have been renamed or deprecated.

  Try:
  1. Search for tables matching &apos;orders&apos;
  2. Check if &apos;orders&apos; or &apos;order_items&apos; fits your query
  3. Ask the user which orders table they want to use&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The LLM handles ambiguity much better when you tell it how to handle ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../images/posts/metabot-tool-use.png&quot; alt=&quot;Diagram showing how tool use improved after the changes were made&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-benchmark-problem-in-ai-analytics-agents&quot;&gt;The benchmark problem in AI analytics agents&lt;/h2&gt;

&lt;p&gt;After the demo disaster, we built benchmarks. Scores climbed into the 90s, but perceived quality dropped.&lt;/p&gt;

&lt;p&gt;The issue is subtle but important: engineers write benchmark prompts like engineers. “Count of orders grouped by created_at week.” Clean, precise, all context provided.&lt;/p&gt;

&lt;p&gt;Real people say: “Why is revenue down?”&lt;/p&gt;

&lt;p&gt;That question is missing a time period, revenue definition, comparison baseline, and probably some context about what triggered the question in the first place. And even if you nail the realistic test cases, there’s the uncontrollable data mess underneath - legacy tables, missing descriptions, inconsistent naming. The gap between benchmark coverage and production chaos is where AI products fail.&lt;/p&gt;

&lt;p&gt;We now treat benchmarks as integration tests, not pure quality measures. If a change drops the score, something broke. But a passing score doesn’t mean the agent works, just that it handles clean inputs correctly. The real evaluation is production feedback, analyzed through a lens of what people actually asked versus what they needed.&lt;/p&gt;

&lt;h2 id=&quot;build-for-chaos-not-happy-paths&quot;&gt;Build for chaos, not happy paths&lt;/h2&gt;

&lt;p&gt;Our initial Metabot hackathon prototype had 5 perfectly documented tables and worked beautifully. Production has hundreds of tables with varying quality, used by people who phrase questions in wildly creative ways, and with edge cases we never imagined.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;../images/posts/metabot-data-warehouse.png&quot; alt=&quot;The imagined clean and simple data warehouse we like to imagine on the left, the broken chaos that is often the reality on the right&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That’s the core lesson: don’t build for the happy path. Every polished demo with clean data creates expectations you can’t meet. People wander off the happy path in seconds.
Better to understand the chaos, build for it, and deliver consistently than to show something impressive that falls apart on contact with reality.&lt;/p&gt;

&lt;p&gt;We learned this the hard way. But it forced us to focus on what actually matters: robust context engineering, handling messy data gracefully, and building for the chaos that production inevitably brings.&lt;/p&gt;

&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try it yourself&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/features/metabase-ai&quot;&gt;Metabot is now out of beta&lt;/a&gt; in Metabase. You can try it with your own data and see how these patterns work in practice. Pro tip: Do your homework and &lt;a href=&quot;/docs/latest/data-modeling/metadata-editing&quot;&gt;set up your semantic types&lt;/a&gt; like foreign key relations, &lt;a href=&quot;/docs/latest/data-modeling/metadata-editing#adding-a-metric&quot;&gt;metrics&lt;/a&gt; and &lt;a href=&quot;/docs/latest/data-modeling/segments-and-metrics&quot;&gt;segments&lt;/a&gt;. This will help improve the experience.&lt;/p&gt;

&lt;p&gt;Want the full technical deep dive? &lt;a href=&quot;https://www.youtube.com/watch?v=EnvozxnWjP4&amp;amp;t=1s&amp;amp;pp=ygUiYWkgZW5naW5lZXIgbWV0YWJvdCB0aG9tYXMgc2NobWlkdA%3D%3D&quot;&gt;Watch the complete talk from AI Engineer Paris 2025&lt;/a&gt; where we go deeper into the implementation details.&lt;/p&gt;

&lt;p&gt;These patterns apply beyond analytics agents. Any time you’re building with LLMs, think about the full context window, deliver instructions when they’re actionable, and always (always!) build for the chaos.&lt;/p&gt;
</description>
        <pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/lessons-learned-building-ai-analytics-agents</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/lessons-learned-building-ai-analytics-agents</guid>
        
        
        <category>Analytics</category>
        
        <category>and</category>
        
        <category>BI</category>
        
      </item>
    
      <item>
        <title>We simplified embedding</title>
        <description>&lt;p&gt;TL;DR: If you’re embedding Metabase and upgrading to 58, &lt;strong&gt;you don’t have to do anything.&lt;/strong&gt; Your existing embeds will continue to work exactly as before. These changes are about simplifying our embedding options so it’s easier for people to pick the option they need.&lt;/p&gt;

&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;/h2&gt;

&lt;p&gt;Starting in &lt;a href=&quot;/releases/metabase-58&quot;&gt;Metabase 58&lt;/a&gt;, there are two ways to embed Metabase for new embeds.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/embedding/modular-embedding&quot;&gt;Modular Embedding&lt;/a&gt;&lt;/strong&gt; - Embed Metabase components, as Guest or as user via SSO.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/docs/latest/embedding/full-app-embedding&quot;&gt;Full-app Embedding&lt;/a&gt;&lt;/strong&gt; - The full Metabase, SSO only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re already embedding Metabase, your existing embeds still work. Static embedding will still work for existing embeds, but new embeds must use Modular Embedding.&lt;/p&gt;

&lt;h2 id=&quot;how-the-old-options-map-to-the-new&quot;&gt;How the old options map to the new&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Before 58&lt;/th&gt;
      &lt;th&gt;Starting in 58&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Static embedding&lt;/td&gt;
      &lt;td&gt;Modular Embedding - Guest&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Embedded analytics JS&lt;/td&gt;
      &lt;td&gt;Modular Embedding - SSO&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Embedded analytics SDK&lt;/td&gt;
      &lt;td&gt;Modular Embedding SDK - Guest or SSO (React only)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Interactive embedding&lt;/td&gt;
      &lt;td&gt;Full-app Embedding - SSO only&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;why-we-changed-our-embedding-options&quot;&gt;Why we changed our embedding options&lt;/h2&gt;

&lt;p&gt;They were confusing. Static embedding was also somewhat interactive, but we also had interactive embedding, which was a different thing. Each had a different setup flow. It worked, but figuring out the right path could have been easier, so that’s what we did. Plus, the new Modular Embedding provides an easier upgrade path from static embedding—you can start with Guest embeds and upgrade to SSO without major code changes.&lt;/p&gt;

&lt;h2 id=&quot;modular-embedding-overview&quot;&gt;Modular Embedding overview&lt;/h2&gt;

&lt;p&gt;We built an in-app wizard that walks you through setup and generates a code snippet. Copy, paste, done. The choice you have to make is whether you give people in your app a Metabase account. If you do, it unlocks a lot of stuff, and reduces your maintenance burden.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Guest&lt;/strong&gt; — People don’t need Metabase accounts. You sign embed URLs with JWT (just like static embedding), and they see the dashboard or question you’ve embedded. You can add and lock filters, but that’s about it. Guest embeds are available in the OSS Edition (with a “Powered by Metabase” badge) and on Pro/Enterprise.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SSO&lt;/strong&gt; — People authenticate through your identity provider and get their own Metabase account. And since your Metabase knows who’s viewing what, it can unlock everything: drill-through, the query builder, AI chat, collection browser, and more, all with the correct permissions applied. Self-service embedded analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modular Embedding has an &lt;a href=&quot;/docs/latest/embedding/sdk/introduction&quot;&gt;SDK for React&lt;/a&gt;, so if your app uses React, you should go with the SDK.&lt;/p&gt;

&lt;p&gt;The nice part: Guest and SSO embeds share the same foundation, so it’s a much smoother upgrade path from Guest to SSO.&lt;/p&gt;

&lt;h2 id=&quot;upgrading-existing-embeds-to-58&quot;&gt;Upgrading existing embeds to 58&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You don’t have to do anything special when upgrading.&lt;/strong&gt; Your existing embeds will continue to work exactly as before.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Static embeds&lt;/strong&gt; — Keep working. No code changes required. The iframe URLs, JWT signing, and parameter handling all work the same way. By default, you’ll see the new Modular Embedding wizard, but there’s an escape hatch: you can still access the legacy static embedding UI, configure embeds with the new wizard, and use the traditional iframe + JWT approach. Migrating to Modular Embedding SSO unlocks deep theming options that weren’t available with static embedding (Pro/Enterprise).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interactive embeds&lt;/strong&gt; — Keep working. Now called “Full-app Embedding,” but nothing changes on your end.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SDK embeds&lt;/strong&gt; — Keep working. No changes to the SDK API. It’s just called the Modular Embedding SDK.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only difference is what you’ll see in the Metabase UI when setting up new embeds.&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/docs/latest/embedding/introduction&quot;&gt;Embedding docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/releases&quot;&gt;Releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/releases/metabase-58&quot;&gt;Metabase 58&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 13 Jan 2026 00:01:00 +0000</pubDate>
        <link>https://www.metabase.com/blog/we-simplified-metabase-embedding</link>
        <guid isPermaLink="true">https://www.metabase.com/blog/we-simplified-metabase-embedding</guid>
        
        
        <category>News</category>
        
      </item>
    
  </channel>
</rss>
