<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Blog on Posit Open Source</title>
    <link>https://opensource.posit.co/blog/</link>
    <description>Recent content in Blog on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <atom:link href="https://opensource.posit.co/blog/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Quarto 1.10</title>
      <link>https://opensource.posit.co/blog/2026-08-03_quarto-1-10/</link>
      <pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-03_quarto-1-10/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>Quarto 1.10 is out! You can get the current release from the <a href="https://quarto.org/docs/download/index.html" target="_blank" rel="noopener">download page</a>.</p>
<p>Quarto 1.10 is dominated by fixes, as much of our development effort is now going into <a href="https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2">Quarto 2</a>, but there are some improvements we wanted to point out: more refined HTML accessibility checks, and access to Quarto&rsquo;s localized strings for template authors.</p>
<p>You can read about these improvements and the most important fixes below. You can find all the changes in this version in the <a href="https://quarto.org/docs/download/changelog/1.10/" target="_blank" rel="noopener">Release Notes</a>.</p>
<h2 id="accessibility-checking-improvements">Accessibility Checking Improvements
</h2>
<p>Since Quarto 1.8, setting the <code>axe</code> option on HTML documents runs <a href="https://github.com/dequelabs/axe-core" target="_blank" rel="noopener">axe-core</a> accessibility checks on your rendered document. Quarto 1.10 makes these checks easier to rely on.</p>
<p><strong>Checking works offline.</strong> Quarto now bundles its own copy of axe-core instead of loading it from a CDN in the reader&rsquo;s browser. Accessibility checking works without a network connection, and viewing a rendered document no longer triggers a request to an external CDN. The axe-core version is unchanged, so results are identical.</p>
<p><strong>Check against a WCAG conformance level.</strong> The new <code>standard</code> option scopes checks to a WCAG conformance level, including rules axe-core keeps off by default for that level. The <code>best-practice</code> option controls whether axe&rsquo;s best-practice rules, recommendations not required by any WCAG success criterion, are also checked:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">document.qmd</span></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">html</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">axe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">standard</span><span class="p">:</span><span class="w"> </span><span class="l">wcag21aa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">best-practice</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div></div>
<p><strong>A clearer report.</strong> When using <code>output: document</code>, violations are now listed first by impact, then WCAG conformance level. The report overlay also received its own accessibility and styling fixes: it can be scrolled with the keyboard, uses theme and brand independent colors, and no longer inherits page styling like text centering.</p>
<p>Learn more at <a href="https://quarto.org/docs/output-formats/html-accessibility.html" target="_blank" rel="noopener">HTML Accessibility</a>.</p>
<h2 id="localized-strings-in-templates">Localized Strings in Templates
</h2>
<p>If you write custom Pandoc templates or <a href="https://quarto.org/docs/journals/templates.html#template-partials" target="_blank" rel="noopener">template partials</a>, you can now use Quarto&rsquo;s localized strings through the new <code>quarto.language</code> template variable namespace. Quarto resolves each string for the document&rsquo;s language and exposes it as a template variable:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">template.typ</span></div><pre tabindex="0"><code class="language-default" data-lang="default">$quarto.language.toc-title-document$
$quarto.language.crossref-fig-title$</code></pre></div>
<p>This lets templates honor the document&rsquo;s <code>lang</code> option without hard-coding text. For example, the <code>orange-book</code> Typst book extension now uses these variables in its running headers, so a book with <code>lang: fr</code> gets <em>Chapitre</em> rather than <em>Chapter</em>.</p>
<p>Learn more at <a href="https://quarto.org/docs/authoring/language.html#localized-strings-in-templates" target="_blank" rel="noopener">Localized Strings in Templates</a>.</p>
<h2 id="other-important-fixes">Other Important Fixes
</h2>
<ul>
<li>
<p><code>quarto preview</code> reliability&mdash;a long-standing bug that showed stale HTML for non-index pages is fixed, changes to a document&rsquo;s <code>format:</code> are detected on the first render after the edit, a <code>_brand.yml</code> added or removed during preview is picked up, and transient <code>.quarto_ipynb</code> files no longer accumulate on disk.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/extensions/shortcodes.html" target="_blank" rel="noopener">Shortcodes</a>&mdash;shortcodes are now resolved inside inline and display math expressions.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/output-formats/pdf-basics.html#fonts" target="_blank" rel="noopener">PDF font fallbacks</a>&mdash;<code>mainfontfallback</code>, <code>sansfontfallback</code>, and <code>monofontfallback</code> no longer crash LuaLaTeX on TeX Live 2026, and a missing fallback font is now installed automatically.</p>
</li>
<li>
<p>Typst fonts&mdash;fonts not installed locally are filtered from CSS <code>font-family</code> fallback lists before they reach Typst, suppressing <code>unknown font family</code> warnings, alongside a batch of fixes to Quarto&rsquo;s CSS-to-Typst translation.</p>
</li>
<li>
<p><a href="https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell">Chrome Headless Shell</a>&mdash;the <code>quarto install chromium</code> command, deprecated in 1.9, now transparently redirects to <code>chrome-headless-shell</code>, <code>quarto check install</code> warns about legacy Chromium installs, and arm64 Linux is now supported.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/websites/website-llms.html" target="_blank" rel="noopener">Output for LLMs</a>&mdash;fixes for websites with <code>llms-txt</code> enabled: headings and shortcodes inside conditional content, and clean-URL rewriting of <code>index.html.md</code> links.</p>
</li>
</ul>
<p>Dependency updates:</p>
<ul>
<li><code>pandoc</code> updated to 3.10</li>
<li><code>typst</code> updated to 0.15.1</li>
<li><code>deno</code> updated to 2.7.14</li>
<li><code>dart-sass</code> updated to 1.101.0</li>
<li><code>esbuild</code> updated to 0.28.1</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We&rsquo;d like to say a huge thank you to everyone who contributed to this release by opening issues and pull requests:</p>
<p><a href="https://github.com/AlexLietard" target="_blank" rel="noopener">AlexLietard</a>,
<a href="https://github.com/ArthurRudolph" target="_blank" rel="noopener">ArthurRudolph</a>,
<a href="https://github.com/Artmann" target="_blank" rel="noopener">Artmann</a>,
<a href="https://github.com/C-Monaghan" target="_blank" rel="noopener">C-Monaghan</a>,
<a href="https://github.com/ChrisJefferson" target="_blank" rel="noopener">ChrisJefferson</a>,
<a href="https://github.com/CoryMcCartan" target="_blank" rel="noopener">CoryMcCartan</a>,
<a href="https://github.com/DCEW" target="_blank" rel="noopener">DCEW</a>,
<a href="https://github.com/DrFloLinke" target="_blank" rel="noopener">DrFloLinke</a>,
<a href="https://github.com/Guest-1013" target="_blank" rel="noopener">Guest-1013</a>,
<a href="https://github.com/MBe-iUS" target="_blank" rel="noopener">MBe-iUS</a>,
<a href="https://github.com/MurzNN" target="_blank" rel="noopener">MurzNN</a>,
<a href="https://github.com/ThierryO" target="_blank" rel="noopener">ThierryO</a>,
<a href="https://github.com/ThomasFaria" target="_blank" rel="noopener">ThomasFaria</a>,
<a href="https://github.com/TinasheMTapera" target="_blank" rel="noopener">TinasheMTapera</a>,
<a href="https://github.com/Voorhoeve" target="_blank" rel="noopener">Voorhoeve</a>,
<a href="https://github.com/alderete" target="_blank" rel="noopener">alderete</a>,
<a href="https://github.com/andrewheiss" target="_blank" rel="noopener">andrewheiss</a>,
<a href="https://github.com/barendgehrels" target="_blank" rel="noopener">barendgehrels</a>,
<a href="https://github.com/basm92" target="_blank" rel="noopener">basm92</a>,
<a href="https://github.com/blauzo" target="_blank" rel="noopener">blauzo</a>,
<a href="https://github.com/chainsawriot" target="_blank" rel="noopener">chainsawriot</a>,
<a href="https://github.com/crisbour" target="_blank" rel="noopener">crisbour</a>,
<a href="https://github.com/cs-res-pub-ser-st" target="_blank" rel="noopener">cs-res-pub-ser-st</a>,
<a href="https://github.com/dustinstoltz" target="_blank" rel="noopener">dustinstoltz</a>,
<a href="https://github.com/eculler" target="_blank" rel="noopener">eculler</a>,
<a href="https://github.com/eneveu" target="_blank" rel="noopener">eneveu</a>,
<a href="https://github.com/apps/github-actions" target="_blank" rel="noopener">github-actions[bot]</a>,
<a href="https://github.com/gregswinehart" target="_blank" rel="noopener">gregswinehart</a>,
<a href="https://github.com/hwine" target="_blank" rel="noopener">hwine</a>,
<a href="https://github.com/ianpittwood" target="_blank" rel="noopener">ianpittwood</a>,
<a href="https://github.com/ihrke" target="_blank" rel="noopener">ihrke</a>,
<a href="https://github.com/jdonaldson" target="_blank" rel="noopener">jdonaldson</a>,
<a href="https://github.com/jiangyun-fun" target="_blank" rel="noopener">jiangyun-fun</a>,
<a href="https://github.com/jidanni" target="_blank" rel="noopener">jidanni</a>,
<a href="https://github.com/jkrumbiegel" target="_blank" rel="noopener">jkrumbiegel</a>,
<a href="https://github.com/jnkatz" target="_blank" rel="noopener">jnkatz</a>,
<a href="https://github.com/jph00" target="_blank" rel="noopener">jph00</a>,
<a href="https://github.com/jtkulas" target="_blank" rel="noopener">jtkulas</a>,
<a href="https://github.com/juleswg23" target="_blank" rel="noopener">juleswg23</a>,
<a href="https://github.com/juliohm" target="_blank" rel="noopener">juliohm</a>,
<a href="https://github.com/kazuyanagimoto" target="_blank" rel="noopener">kazuyanagimoto</a>,
<a href="https://github.com/kelli-rstudio" target="_blank" rel="noopener">kelli-rstudio</a>,
<a href="https://github.com/lsbjordao" target="_blank" rel="noopener">lsbjordao</a>,
<a href="https://github.com/luismmontilla" target="_blank" rel="noopener">luismmontilla</a>,
<a href="https://github.com/maelle" target="_blank" rel="noopener">maelle</a>,
<a href="https://github.com/maucejo" target="_blank" rel="noopener">maucejo</a>,
<a href="https://github.com/memeplex" target="_blank" rel="noopener">memeplex</a>,
<a href="https://github.com/micedre" target="_blank" rel="noopener">micedre</a>,
<a href="https://github.com/multimeric" target="_blank" rel="noopener">multimeric</a>,
<a href="https://github.com/nathant181" target="_blank" rel="noopener">nathant181</a>,
<a href="https://github.com/neilernst" target="_blank" rel="noopener">neilernst</a>,
<a href="https://github.com/nessan" target="_blank" rel="noopener">nessan</a>,
<a href="https://github.com/nickvigilante" target="_blank" rel="noopener">nickvigilante</a>,
<a href="https://github.com/nrennie" target="_blank" rel="noopener">nrennie</a>,
<a href="https://github.com/pbosetti" target="_blank" rel="noopener">pbosetti</a>,
<a href="https://github.com/reckoner" target="_blank" rel="noopener">reckoner</a>,
<a href="https://github.com/rgouveiamendes" target="_blank" rel="noopener">rgouveiamendes</a>,
<a href="https://github.com/robjhyndman" target="_blank" rel="noopener">robjhyndman</a>,
<a href="https://github.com/sbwiecko" target="_blank" rel="noopener">sbwiecko</a>,
<a href="https://github.com/seandavi" target="_blank" rel="noopener">seandavi</a>,
<a href="https://github.com/sebastiansauer" target="_blank" rel="noopener">sebastiansauer</a>,
<a href="https://github.com/skyfroger" target="_blank" rel="noopener">skyfroger</a>,
<a href="https://github.com/songwupei" target="_blank" rel="noopener">songwupei</a>,
<a href="https://github.com/stefkuypers" target="_blank" rel="noopener">stefkuypers</a>,
<a href="https://github.com/stragu" target="_blank" rel="noopener">stragu</a>,
<a href="https://github.com/tiagojct" target="_blank" rel="noopener">tiagojct</a>,
<a href="https://github.com/victorrssx" target="_blank" rel="noopener">victorrssx</a>,
<a href="https://github.com/widlarizer" target="_blank" rel="noopener">widlarizer</a>,
<a href="https://github.com/wlatendresse" target="_blank" rel="noopener">wlatendresse</a>,
<a href="https://github.com/xuefeng-xu" target="_blank" rel="noopener">xuefeng-xu</a>,
<a href="https://github.com/yasyf" target="_blank" rel="noopener">yasyf</a>,
<a href="https://github.com/zinc75" target="_blank" rel="noopener">zinc75</a>.</p>
<p>The hammer and wrench emoji in the <a href="thumbnail.png">listing and social card image</a> for this post comes from <a href="https://openmoji.org/" class="external">OpenMoji</a>&ndash; the open-source emoji and icon project. License: <a href="https://creativecommons.org/licenses/by-sa/4.0/#" class="external">CC BY-SA 4.0</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-03_quarto-1-10/thumbnail.png" length="48234" type="image/png" />
    </item>
    <item>
      <title>AI Newsletter: EDA log in Posit Assistant</title>
      <link>https://opensource.posit.co/blog/2026-07-31_ai-newsletter/</link>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-31_ai-newsletter/</guid>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-body">
<p><strong>Subscribe to the AI Newsletter!</strong></p>
<p>The AI newsletter is now published as an RSS feed. Follow it in your favorite reader:</p>
<p><a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener noreferrer" class="btn-shortcode inline-flex mb-5 mr-5 items-center px-4 py-3 text-sm leading-5 gap-2 rounded-lg bg-blue-400 !text-white font-semibold align-middle hover:bg-blue-500 transition no-underline">Subscribe via RSS</a></p>
<p><strong>Want the newsletter as an email?</strong> Paste the feed URL — <a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener">https://opensource.posit.co/tags/ai-newsletter/index.xml</a> — into a free RSS-to-email service such as <a href="https://blogtrottr.com/" target="_blank" rel="noopener">Blogtrottr</a>, <a href="https://feedrabbit.com/" target="_blank" rel="noopener">Feedrabbit</a>, or <a href="https://follow.it/" target="_blank" rel="noopener">Follow.it</a>, and each new issue will arrive in your inbox.</p>
</div>
</div>
<br>
[Posit Assistant](https://assistant.posit.co/) in Positron now includes an EDA log feature to help you keep track of exploratory analysis done with the agent.
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-07-31_ai-newsletter/images/eda-log-zoom.png"
      alt="Screenshot of Positron. On the left, Posit Assistant has analyzed a dataset of U.S. language speakers, showing a bar chart and written findings. On the right, the EDA Log opens in an editor tab titled &ldquo;ACS language speakers&rdquo;: a table with Area, Status, and Notes columns lists three areas—&ldquo;Dataset structure &amp; quality&rdquo; and &ldquo;Top languages nationwide&rdquo; marked Explored, and &ldquo;Language coverage across states&rdquo; marked Partial—each with bullet-point findings and an arrow that links back to the conversation, followed by a &ldquo;Next steps&rdquo; section of suggested directions." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The log summarizes findings for different areas of exploration and keeps track of next steps. To use the log, run the <code>/eda-log</code> slash command after you&rsquo;ve started the EDA process.</p>
<h3 id="why-we-made-this">Why we made this
</h3>
<p>Exploratory data analysis, the open-ended orientation to your data that often comes before anything else, can be a branching, nonlinear process. There are many questions you can ask of your data, and new areas of inquiry can open with each question you ask. Because of this, it is often hard to keep track of what you&rsquo;ve looked into, where that code lives, and what you want to explore next.</p>
<p>Historically, the EDA process was limited by how quickly you could write code and interpret the output. Coding agents like Posit Assistant lift the first of those constraints. They can carry out EDA far faster than you can on your own, which can exacerbate the issue of keeping track of what you&rsquo;ve explored.</p>
<p>This speed also introduces a new problem: the point of EDA is typically for you, the human, to understand your data, but coding agents can produce output faster than you can absorb it. If the agent completes an analysis but you haven&rsquo;t understood the insights in the data, the exploration process hasn&rsquo;t really happened.</p>
<p>Posit Assistant already has various features that tackle this problem, including an exploratory mode of interaction where it runs shorter turns and stops more frequently to involve the user.</p>
<p>The EDA log is another lightweight tool for the same goal. It keeps a running summary of what you and Posit Assistant have explored, helping your understanding keep pace with the agent&rsquo;s and giving you a clearer picture of what&rsquo;s already been done.</p>
<h3 id="details">Details
</h3>
<p>Here&rsquo;s what the EDA log looks like in action:</p>
<script src="https://fast.wistia.com/player.js" async></script>
<script src="https://fast.wistia.com/embed/bu9ch5gqvx.js" async type="module"></script>
<style>wistia-player[media-id='bu9ch5gqvx']:not(:defined) { background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/bu9ch5gqvx/swatch'); display: block; filter: blur(5px); padding-top:60.42%; }</style>
<p><wistia-player media-id="bu9ch5gqvx" aspect="1.6551724137931034"></wistia-player></p>
<p>At a high level:</p>
<ul>
<li>When you run <code>/eda-log</code>, Posit Assistant will create a log for the exploration done in the conversation so far. The log then opens in the editor area in Positron.</li>
<li>The underlying logs are stored as YAML files in <code>.posit/assistant/eda-logs/</code>, next to where plans are stored.</li>
<li>Posit Assistant is instructed to loosely keep the log up to date as the conversation progresses, but you can also manually trigger an update at any time with the &ldquo;Refresh&rdquo; button.</li>
<li>Clicking the arrow next to an area scrolls you back to the spot in the conversation where that insight originated, so you can revisit the code and context that produced it.</li>
<li>Suggested next steps appear as clickable text. Clicking one sends it to Posit Assistant as your next message.</li>
<li>The creation of an EDA log is always user-triggered. Posit Assistant will never create one on its own.</li>
<li>The feature is currently only in Positron, but will come to RStudio soon.</li>
</ul>
<h2 id="recent-past-newsletters">Recent past newsletters
</h2>
<ul>
<li><a href="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/">Which models are best at spotting data quality problems?</a></li>
<li><a href="https://opensource.posit.co/blog/2026-07-03_ai-newsletter/">How to choose between AGENTS.md, skills, and MCP servers</a></li>
</ul>
<br>
<br>
<p><a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener noreferrer" class="btn-shortcode inline-flex mb-5 mr-5 items-center px-4 py-3 text-sm leading-5 gap-2 rounded-lg bg-blue-400 text-white font-semibold align-middle hover:bg-blue-500 transition no-underline">Subscribe via RSS</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-31_ai-newsletter/images/hero.png" length="954735" type="image/png" />
    </item>
    <item>
      <title>Positron&#39;s Jupyter Notebook Editor Is Now Generally Available</title>
      <link>https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/</link>
      <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/</guid>
      <dc:creator>Wasim Lorgat</dc:creator>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Data scientists are often forced to choose between the simplicity of the classic Jupyter notebook experience and the advanced capabilities of a general-purpose IDE. With the <a href="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/" target="_blank" rel="noopener">2026.07 release</a>, you no longer have to choose: Positron now delivers a first-class Notebook Editor inside an IDE built for data science.</p>
<p>If you&rsquo;re new to Positron, here&rsquo;s what the Notebook Editor is like in your daily work.</p>
<h2 id="environment-management">Environment management
</h2>
<p>You clone your colleague&rsquo;s repo and try to rerun their notebook. Which Python version do you use? What dependencies do you install and which versions? Did you previously set up an environment that you could reuse? You&rsquo;re faced with all of these questions before you&rsquo;ve run a single line of code.</p>
<p>Positron has several <a href="https://opensource.posit.co/blog/2026-07-08_positron-uv/" target="_blank" rel="noopener">built-in workflows</a> to help you manage your environments. When you open a repo, Positron discovers your installed environments, suggests the right one for the repo, and prompts you to set up a new environment if needed. The active environment is displayed front and center; click to restart or switch environments. Positron uses the same environment across your Jupyter and Quarto notebooks, scripts, and consoles. You can see inside your environments, which packages are installed, their versions, whether newer versions are available, and you can upgrade them. Environments are first-class citizens of Positron. <a href="https://positron.posit.co/positron-notebook-editor.html#setting-up-your-environment" target="_blank" rel="noopener">Explore Positron&rsquo;s environment management</a>.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-from-git.gif" data-fig-align="center" data-fig-alt="Positron helps you resolve environment and package dependencies as you pull down a colleague&#39;s notebook" />
<h2 id="interactive-data-exploration">Interactive data exploration
</h2>
<p>When you run a cell in your notebook, your variables appear in the <a href="https://positron.posit.co/variables-pane.html" target="_blank" rel="noopener">Variables Pane</a>. Filter, sort, and search your data in the <a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>, and try out plot variations in the Visualize wizard. You can easily convert your point-and-click interactions into code by using the copy-code feature to bring them back into your notebook. If you get stuck, look up library documentation in the <a href="https://positron.posit.co/help-pane.html" target="_blank" rel="noopener">Help Pane</a>. All of this works out of the box.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-data-exploration.gif" data-fig-align="center" data-fig-alt="Run a notebook and see your variables update live, inspect them in the data explorer and convert filters back to code." />
<h2 id="streamlined-version-control">Streamlined version control
</h2>
<p>Notebooks store outputs and execution metadata alongside code, which is great for sharing but does not work well with version control. Positron includes settings to exclude outputs and execution metadata from the saved file, so the diff is just your code change. <a href="https://positron.posit.co/positron-notebook-editor.html#version-control" target="_blank" rel="noopener">Explore the custom settings</a> to clean up your git diffs.</p>
<p>Your notebooks stay ordinary <code>.ipynb</code> files and the editing experience stays classic Jupyter. When a plain-text workflow suits you better, conversion to and from <code>.ipynb</code> and other formats is built in.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-save-settings.png" data-fig-align="center" data-fig-alt="Customize what is saved in your notebooks for better version control." />
<h2 id="ai-assistance">AI assistance
</h2>
<p>Posit Assistant sees your notebook as more than just text. It works with the live session behind the notebook: your variables, your data, your plots. Ask it to fix a chart and it can inspect the dataframe, look at the plot itself, edit the cell, and run it again. It does the same things you would do, using the same panes you use. <a href="https://positron.posit.co/positron-notebook-editor.html#ai-integration" target="_blank" rel="noopener">Explore notebook-aware AI assistance in Positron</a>.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-ai.gif" data-fig-align="center" data-fig-alt="Assistant can fix, explain, run your notebook and suggest next steps." />
<h2 id="get-started">Get started
</h2>
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> and open a project with a Jupyter notebook. The Notebook Editor is now on by default. If you&rsquo;re coming from another IDE, your existing <code>.ipynb</code> files will open as-is without any conversion needed.</p>
<p>For the full feature reference, check out our <a href="https://positron.posit.co" target="_blank" rel="noopener">documentation</a>.</p>
<p>If you run into any issues or have ideas on how we can improve Positron, reach out on <a href="https://github.com/posit-dev/positron" target="_blank" rel="noopener">GitHub</a>.</p>
<p>Already using Positron? Everything in this release is in the <a href="https://positron.posit.co/release-notes" target="_blank" rel="noopener">release notes</a>.</p>
<p>Thank you so much for helping us build Positron&rsquo;s Jupyter notebook support from the ground up by chatting with us live, sharing your pain points, and giving us feedback on GitHub!</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> to try out the Notebook Editor and other new features!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/featured.png" length="686743" type="image/png" />
    </item>
    <item>
      <title>Introducing lorax: Speaking for the Tree-Based Models</title>
      <link>https://opensource.posit.co/blog/2026-07-28_lorax/</link>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-28_lorax/</guid>
      <dc:creator>Max Kuhn</dc:creator><description><![CDATA[<p>When building tree-based models, such as <a href="https://cran.r-project.org/package=rpart" target="_blank" rel="noopener">CART</a>, <a href="https://en.wikipedia.org/wiki/Random_forest" target="_blank" rel="noopener">random forests</a>, or <a href="https://en.wikipedia.org/wiki/XGBoost" target="_blank" rel="noopener">XGBoost</a>, we might be interested in knowing a little more about how the model works. For example, if a CART and XGBoost model had roughly the same performance, we might want to characterize how complex each is so that we are more informed about which to prefer. Knowing how many predictors were used, how many terminal nodes are in the tree, and similar characteristics can help understand the model. We might desire to visualize the tree (or a tree in the ensemble), and so on.</p>
<p>The problem is that many packages store the tree&rsquo;s splits in different ways or offer incompatible APIs to access different characteristics. lorax helps capture this information for many different implementations.</p>
<p>You can install the CRAN version of lorax via</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;lorax&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>or get the development version using</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="s">&#34;tidymodels/lorax&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Let&rsquo;s start by using the ranger package to create a random forest model using the food delivery data in the modeldata package. For illustration, the trees will be coerced to be more shallow than usual so that we can better plot them using the <code>min.node.size</code> argument:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span> <span class="c1"># &lt;- to easily get dplyr, tidyr, ggplot2, etc</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ranger</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">lorax</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">872</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_fit</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">ranger</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">time_to_delivery</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">num.trees</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min.node.size</span> <span class="o">=</span> <span class="m">5000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">importance</span> <span class="o">=</span> <span class="s">&#34;impurity&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rgr_fit</span></span></span></code></pre></div></div>
<pre><code>Ranger result

Call:
 ranger(time_to_delivery ~ ., data = modeldata::deliveries, num.trees = 1000,      min.node.size = 5000, importance = &quot;impurity&quot;) 

Type:                             Regression 
Number of trees:                  1000 
Sample size:                      10012 
Number of independent variables:  30 
Mtry:                             5 
Target node size:                 5000 
Variable importance mode:         impurity 
Splitrule:                        variance 
OOB prediction error (MSE):       24.26389 
R squared (OOB):                  0.4879119 
</code></pre>
<h2 id="visualizing-the-trees">Visualizing the Trees
</h2>
<p>lorax contains methods for the <code>as.party()</code> function in the partykit package. This enables us to use all of the methods from that package. For example, <code>plot.party()</code> is an excellent visualization tool for the tree. Random forest has <em>many</em> trees and we can plot any of them:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_fit</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">as.party</span><span class="p">(</span><span class="n">tree</span> <span class="o">=</span> <span class="m">1</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">plot</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-07-28_lorax/index.markdown_strict_files/figure-markdown_strict/rgr-plots-1.png" style="width:100.0%" data-fig-alt="Diagram of tree 1 from the random forest. Internal nodes show the splitting predictors with branches labeled by split values, ending in boxplots of the delivery time distribution in each terminal node." />
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_fit</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">as.party</span><span class="p">(</span><span class="n">tree</span> <span class="o">=</span> <span class="m">100</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">plot</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-07-28_lorax/index.markdown_strict_files/figure-markdown_strict/rgr-plots-2.png" style="width:100.0%" data-fig-alt="Diagram of tree 100 from the random forest, in the same format but with different splits and predictors." />
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_fit</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">as.party</span><span class="p">(</span><span class="n">tree</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">plot</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-07-28_lorax/index.markdown_strict_files/figure-markdown_strict/rgr-plots-3.png" style="width:100.0%" data-fig-alt="Diagram of tree 1000 from the random forest, in the same format but with different splits and predictors." />
<h2 id="which-predictors-were-used">Which Predictors Were Used?
</h2>
<p>Since trees automatically conduct <em>feature selection</em> as the model is trained, it helps to know which ones are <em>actually</em> used by the model. The <code>active_predictors()</code> function does just that:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_vars</span> <span class="o">&lt;-</span> <span class="nf">active_predictors</span><span class="p">(</span><span class="n">rgr_fit</span><span class="p">,</span> <span class="n">tree</span> <span class="o">=</span> <span class="m">1</span><span class="o">:</span><span class="m">1000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_vars</span></span></span></code></pre></div></div>
<pre><code># A tibble: 1,000 × 2
   active_predictors  tree
   &lt;list&gt;            &lt;int&gt;
 1 &lt;chr [4]&gt;             1
 2 &lt;chr [5]&gt;             2
 3 &lt;chr [3]&gt;             3
 4 &lt;chr [4]&gt;             4
 5 &lt;chr [4]&gt;             5
 6 &lt;chr [3]&gt;             6
 7 &lt;chr [3]&gt;             7
 8 &lt;chr [7]&gt;             8
 9 &lt;chr [3]&gt;             9
10 &lt;chr [6]&gt;            10
# ℹ 990 more rows
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># The column names are in a nested vector:</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_vars</span><span class="o">$</span><span class="n">active_predictors[[1]]</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;day&quot;     &quot;hour&quot;    &quot;item_10&quot; &quot;item_23&quot;
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># We can expand the list too:</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_vars</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">unnest</span><span class="p">(</span><span class="n">cols</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="n">active_predictors</span><span class="p">))</span></span></span></code></pre></div></div>
<pre><code># A tibble: 4,306 × 2
   active_predictors  tree
   &lt;chr&gt;             &lt;int&gt;
 1 day                   1
 2 hour                  1
 3 item_10               1
 4 item_23               1
 5 day                   2
 6 distance              2
 7 item_10               2
 8 item_12               2
 9 item_26               2
10 hour                  3
# ℹ 4,296 more rows
</code></pre>
<p>How often are predictors used?</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_vars</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">unnest</span><span class="p">(</span><span class="n">cols</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="n">active_predictors</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">count</span><span class="p">(</span><span class="n">active_predictors</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">arrange</span><span class="p">(</span><span class="n">active_predictors</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 30 × 2
   active_predictors     n
   &lt;chr&gt;             &lt;int&gt;
 1 day                 516
 2 distance            507
 3 hour                623
 4 item_01             329
 5 item_02             131
 6 item_03              57
 7 item_04              66
 8 item_05              26
 9 item_06              92
10 item_07              91
# ℹ 20 more rows
</code></pre>
<p>How many predictors are used in each rule (on average)? A rule is the full logical statement that defines the path to the terminal nodes.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_vars</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">mutate</span><span class="p">(</span><span class="n">num_vars</span> <span class="o">=</span> <span class="nf">map_int</span><span class="p">(</span><span class="n">active_predictors</span><span class="p">,</span> <span class="o">~</span> <span class="nf">length</span><span class="p">(</span><span class="n">.x</span><span class="p">)))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">summarize</span><span class="p">(</span><span class="n">mean_num_vars</span> <span class="o">=</span> <span class="nf">mean</span><span class="p">(</span><span class="n">num_vars</span><span class="p">))</span></span></span></code></pre></div></div>
<pre><code># A tibble: 1 × 1
  mean_num_vars
          &lt;dbl&gt;
1          4.31
</code></pre>
<p>Many packages can compute <em>variable importance scores</em> for each predictor but each has a different interface. The lorax package has an accessor function to pull these from the model called <code>var_imp()</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">var_imp</span><span class="p">(</span><span class="n">rgr_fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 30 × 2
   term     estimate
   &lt;chr&gt;       &lt;dbl&gt;
 1 hour     108892. 
 2 day       16054. 
 3 distance  25689. 
 4 item_01     673. 
 5 item_02      86.3
 6 item_03      30.4
 7 item_04      43.7
 8 item_05      11.0
 9 item_06      50.1
10 item_07      55.2
# ℹ 20 more rows
</code></pre>
<h2 id="examining-rules">Examining Rules
</h2>
<p>We can also get detailed information on the model&rsquo;s rules (for each tree). Using the same ranger model fit:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_rules</span> <span class="o">&lt;-</span> <span class="nf">extract_rules</span><span class="p">(</span><span class="n">rgr_fit</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_rules</span></span></span></code></pre></div></div>
<pre><code># A tibble: 5 × 3
     id rules       tree
  &lt;int&gt; &lt;list&gt;     &lt;int&gt;
1     3 &lt;language&gt;     1
2     5 &lt;language&gt;     1
3     7 &lt;language&gt;     1
4     8 &lt;language&gt;     1
5     9 &lt;language&gt;     1
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># With components</span>
</span></span><span class="line"><span class="cl"><span class="n">rgr_rules</span><span class="o">$</span><span class="n">rules[[1]]</span> <span class="o">|&gt;</span> <span class="nf">class</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;call&quot;
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rgr_rules</span><span class="o">$</span><span class="n">rules[[1]]</span></span></span></code></pre></div></div>
<pre><code>item_23 &lt;= 0.5 &amp; hour &lt;= 14.6645
</code></pre>
<p>We can compute on these and/or use them to determine which specific data were contained in the terminal node:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">modeldata</span><span class="o">::</span><span class="n">deliveries</span> <span class="o">|&gt;</span> <span class="nf">filter</span><span class="p">(</span><span class="o">!!</span><span class="n">rgr_rules</span><span class="o">$</span><span class="n">rules[[1]]</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 2,415 × 31
   time_to_delivery  hour day   distance item_01 item_02 item_03 item_04 item_05
              &lt;dbl&gt; &lt;dbl&gt; &lt;fct&gt;    &lt;dbl&gt;   &lt;int&gt;   &lt;int&gt;   &lt;int&gt;   &lt;int&gt;   &lt;int&gt;
 1             16.1  11.9 Thu       3.15       0       0       2       0       0
 2             19.6  13.0 Sat       3.35       1       0       0       1       0
 3             17.4  11.9 Sun       2.75       0       2       1       0       0
 4             18.0  12.1 Tue       2.4        0       0       0       1       0
 5             22.1  14.4 Thu       2.69       0       0       0       0       0
 6             17.6  12.9 Sat       2.47       0       1       0       0       0
 7             17.0  12.3 Sat       3.88       0       0       0       0       0
 8             19.5  13.5 Tue       3.55       0       0       0       0       0
 9             17.6  12.9 Fri       2.88       0       0       1       1       0
10             21.6  14.3 Sat       3          0       0       0       1       0
# ℹ 2,405 more rows
# ℹ 22 more variables: item_06 &lt;int&gt;, item_07 &lt;int&gt;, item_08 &lt;int&gt;,
#   item_09 &lt;int&gt;, item_10 &lt;int&gt;, item_11 &lt;int&gt;, item_12 &lt;int&gt;, item_13 &lt;int&gt;,
#   item_14 &lt;int&gt;, item_15 &lt;int&gt;, item_16 &lt;int&gt;, item_17 &lt;int&gt;, item_18 &lt;int&gt;,
#   item_19 &lt;int&gt;, item_20 &lt;int&gt;, item_21 &lt;int&gt;, item_22 &lt;int&gt;, item_23 &lt;int&gt;,
#   item_24 &lt;int&gt;, item_25 &lt;int&gt;, item_26 &lt;int&gt;, item_27 &lt;int&gt;
</code></pre>
<h2 id="a-list-of-methods">A List of Methods
</h2>
<p>Here are the details of which models and methods are supported in the first CRAN version:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">class</th>
          <th style="text-align: left">var_imp</th>
          <th style="text-align: left">active_predictors</th>
          <th style="text-align: left">as.party</th>
          <th style="text-align: left">extract_rules</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">bart</td>
          <td style="text-align: left">n/a</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">C5.0</td>
          <td style="text-align: left">n/a</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">cforest</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">n/a</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">cubist</td>
          <td style="text-align: left">✖</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✖</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">grf</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">lgb.Booster</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">ObliqueForest</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✖</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">party</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">n/a</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">randomForest</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">ranger</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">rpart</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">n/a</td>
          <td style="text-align: left">✔</td>
      </tr>
      <tr>
          <td style="text-align: left">xgb.Booster</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
          <td style="text-align: left">✔</td>
      </tr>
  </tbody>
</table>
<p>Note that <code>as.party.rpart()</code> is in the partykit package and that cforest is made out of party objects.</p>
<h2 id="whats-next">What&rsquo;s Next?
</h2>
<p>We&rsquo;ll work on adding <a href="https://catboost.ai/" target="_blank" rel="noopener">CatBoost</a> models to the list of supported methods. Please <a href="https://github.com/tidymodels/lorax/issues" target="_blank" rel="noopener">add an issue</a> if there are other aspects of trees that should be quantified in these models.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-28_lorax/featured.png" length="511489" type="image/png" />
    </item>
    <item>
      <title>ir 0.1.0: self-describing R scripts and Quarto documents</title>
      <link>https://opensource.posit.co/blog/2026-07-23_ir-0-1-0/</link>
      <pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-23_ir-0-1-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator>
      <dc:creator>Christophe Dervieux</dc:creator>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<style>
.chroma code[data-lang="r"] .c1 {
  font-style: normal;
}
</style>
<p>Today we are announcing the first public release of <code>ir</code>, a small
command-line tool that runs R scripts and renders Quarto documents using
runtime requirements declared in the file itself.</p>
<p><code>ir</code> is for one-file workflows that do not quite need a project but
still need to be easy to share and rerun later. Simply put the package
requirements, and optionally the R version, next to the code. When you
render or execute the file, <code>ir</code> resolves the requirements, prepares
cached package libraries, and launches R or Quarto with a runtime ready
to use.</p>
<p>The interface is inspired by
<a href="https://peps.python.org/pep-0723/" target="_blank" rel="noopener">PEP 723</a> and
<a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"><code>uv run --script</code></a>. A script can carry
enough metadata to describe its runtime, and the runner can resolve that
runtime into a cached environment on demand. <code>ir</code> brings that pattern to
R scripts and Quarto documents, using <code>pak</code>, <code>renv</code>, and <code>rig</code> on the R
side, and <code>reticulate</code>&rsquo;s uv-backed helper when Python is part of the
runtime.</p>
<p><code>ir</code> focuses on two workflows:</p>
<ul>
<li>running or rendering self-describing scripts and documents <br>
(<code>ir run</code>, <code>ir render</code>)</li>
<li>running or installing command-line tools distributed through R
packages <br>
(<code>rx</code>, <code>ir tool install</code>)</li>
</ul>
<h2 id="why-ir">Why <code>ir</code>?
</h2>
<p>R scripts often begin as small, local utilities: a report, a data pull,
a model-training or evaluation run, a quick diagnostic, or an example
shared with a colleague. Over time, the script can become important, but
the setup still lives somewhere else: in a README, in a shell history,
in a project library, or in the author&rsquo;s current R installation.</p>
<p><code>ir</code> makes the runtime specification part of the source file. That means
a script can say, directly:</p>
<ul>
<li>which packages it needs</li>
<li>which R should run it, when that needs to be explicit</li>
<li>whether user libraries should be visible</li>
<li>whether CRAN packages should be resolved as of a specific date</li>
</ul>
<p>This should help you rerun the script reliably at a later date. Keeping
the metadata in the file makes it less likely to be lost or fall out of
sync with the code.</p>
<h2 id="how-ir-fits-with-existing-tools">How <code>ir</code> fits with existing tools
</h2>
<p><code>ir</code> sits alongside the R tools people already use and builds on several
of them directly:</p>
<ul>
<li><a href="https://github.com/r-lib/rig" target="_blank" rel="noopener"><code>rig</code></a> installs, removes, and switches
between R versions on macOS, Windows, and Linux. <code>ir</code> calls <code>rig</code> when
a file requests a specific R version, or when date-only
<code>exclude-newer</code> needs to select the latest R minor version available
on that date. <code>rig</code> is optional for files that only declare packages.</li>
<li><a href="https://pak.r-lib.org/" target="_blank" rel="noopener"><code>pak</code></a> is a fast package installer with a
built-in solver. <code>ir</code> uses <code>pak</code> to resolve the dependency graph from
a file&rsquo;s declared packages and fetch them from the appropriate
repositories. <code>pak</code> is bootstrapped automatically on first use, so you
do not need to install it separately.</li>
<li><a href="https://rstudio.github.io/renv/" target="_blank" rel="noopener"><code>renv</code></a> gives R projects isolated
package libraries and lockfiles. <code>ir</code> uses <code>renv</code>&rsquo;s global package
cache to assemble reusable libraries without creating a <code>renv</code> project
or lockfile.</li>
</ul>
<p><code>ir</code> builds on that stack for non-project workflows: resolving the
runtime for a self-describing script or Quarto document, and running or
installing command-line entry points distributed by R packages.</p>
<h2 id="a-self-describing-r-script">A self-describing R script
</h2>
<p>Here is a complete script:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env -S ir run</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| packages:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   - dplyr&gt;=1.0</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   - tidyr</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| isolated: true</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| exclude-newer: &#34;2024-01-15&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mtcars</span> <span class="o">|&gt;</span> <span class="nf">count</span><span class="p">(</span><span class="n">cyl</span><span class="p">,</span> <span class="n">gear</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="nf">pivot_wider</span><span class="p">(</span><span class="n">names_from</span> <span class="o">=</span> <span class="n">gear</span><span class="p">,</span> <span class="n">values_from</span> <span class="o">=</span> <span class="n">n</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Run it with:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ir run script.R
</span></span></code></pre></div></div>
<p>Or, on macOS and Linux, make it executable and run it directly:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> chmod +x script.R
</span></span><span class="line"><span class="cl"><span class="gp">$</span> ./script.R
</span></span></code></pre></div></div>
<p>The metadata block is YAML written in <code>#|</code> comments after an optional
shebang. <code>ir</code> reads that metadata, resolves the declared packages with
<code>pak</code>, materializes a package library with <code>renv</code>, and starts R with the
resolved library at the front of <code>.libPaths()</code>.</p>
<p>By default, user libraries remain visible as a fallback. Add
<code>isolated: true</code> in the file, or use <code>--isolated</code> at the command line,
to run without the user library.</p>
<h2 id="running-r-package-tools-with-rx">Running R package tools with <code>rx</code>
</h2>
<p>The <code>ir</code> release also includes <code>rx</code>, a short alias for <code>ir tool run</code>
that runs executables provided by R packages.</p>
<p>Package authors can expose command-line entry points through standard
package subdirectories such as <a href="https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Package-subdirectories-1" target="_blank" rel="noopener"><code>exec/</code> and <code>bin/</code></a>. Files in <code>exec/</code> can be regular <code>Rscript</code> files,
<a href="https://github.com/r-lib/Rapp" target="_blank" rel="noopener"><code>Rapp</code></a> apps, or direct executable
scripts. <code>rx</code> resolves the package, finds the requested executable, and
runs it in an isolated library.</p>
<p>For example, this resolves the <code>btw</code> CLI from the <code>btw</code> R package on
demand and runs <code>btw --help</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> rx btw --help
</span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">#</span> same as:
</span></span><span class="line"><span class="cl"><span class="gp">$</span> ir tool run btw --help
</span></span></code></pre></div></div>
<p>For tools you use regularly, install them with <code>ir tool install</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">#</span> run once
</span></span><span class="line"><span class="cl"><span class="gp">$</span> ir tool install btw
</span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">#</span> now you only need
</span></span><span class="line"><span class="cl"><span class="gp">$</span> btw --help
</span></span></code></pre></div></div>
<h2 id="cached-by-design">Cached by design
</h2>
<p>The first run of a new dependency set does the normal work of resolving
and installing packages. Later runs reuse cached resolutions and
content-addressed package libraries when the same requirements are seen
again.</p>
<p>That makes <code>ir</code> useful for both one-off and repeated command-line work.
You can run a script, run an inline expression, render a report, or
launch a package-provided executable without creating a project
directory just to hold the dependency state.</p>
<p><code>ir</code> also bootstraps its own resolver tooling on first use, so you do
not need to pre-install <code>pak</code> or <code>renv</code>.</p>
<h2 id="reproducibility-without-a-project">Reproducibility without a project
</h2>
<p>For R scripts that need more explicit reproducibility, <code>exclude-newer</code>
is usually the first thing to reach for:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">exclude-newer</span><span class="p">:</span><span class="w"> </span><span class="ld">2024-01-15</span></span></span></code></pre></div></div>
<p>You can also provide the same date at the command line:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ir run --exclude-newer 2024-01-15 script.R
</span></span></code></pre></div></div>
<p>This resolves packages from the Posit Package Manager snapshot for that
date. When no other R selector is set, the same date also tells <code>ir</code> to
select the latest R minor version available on that date. If you were
writing with the current release of R and current CRAN packages, the
date alone is usually enough.</p>
<p>Use <code>r-version</code> when the script really needs a specific installed R
version or version range. This selection uses
<a href="https://github.com/r-lib/rig" target="_blank" rel="noopener"><code>rig</code></a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">r-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;4.3&#34;</span></span></span></code></pre></div></div>
<p>Together, these options let a file carry the important parts of its
runtime requirements without needing a surrounding project. For
reproducibility, most files should need only a list of packages and a
date.</p>
<h2 id="quarto-documents-too">Quarto documents too
</h2>
<p><code>ir</code> uses the same metadata model for Quarto documents. Put package
metadata under an <code>ir:</code> key in the document YAML:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">My report</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">packages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">dplyr&gt;=1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">gt@1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">isolated</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">exclude-newer</span><span class="p">:</span><span class="w"> </span><span class="ld">2025-05-15</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></div></div>
<p>Then render with:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ir render report.qmd
</span></span><span class="line"><span class="cl"><span class="gp">$</span> ir render report.qmd --to pdf
</span></span></code></pre></div></div>
<p>When <code>ir</code> selects an R executable using <code>--r-version</code>, frontmatter
<code>ir.r-version</code>, or date-only <code>exclude-newer</code>, it sets <a href="https://quarto.org/docs/advanced/environment-vars.html#variables-quarto-inspects" target="_blank" rel="noopener"><code>QUARTO_R</code></a> so Quarto renders with that R. <code>ir</code> also seeds <code>rmarkdown</code>
automatically for knitr-based renders unless you declare it yourself.</p>
<h2 id="python-environments-too">Python environments too
</h2>
<p>Some R scripts and Quarto documents also need Python. In an R script,
declare the R packages and Python requirements in the same metadata
block:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env -S ir run</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| packages:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   - reticulate</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| python-packages:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   - pandas</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   - matplotlib</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| python-version: &#34;3.11&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| exclude-newer: &#34;2026-06-01&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">reticulate</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">pd</span> <span class="o">&lt;-</span> <span class="nf">import</span><span class="p">(</span><span class="s">&#34;pandas&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p><code>ir</code> creates the environment with <code>reticulate</code>&rsquo;s uv-backed environment
helper, sets <code>RETICULATE_PYTHON</code>, and activates the environment for
subprocesses. Declare <code>reticulate</code> under <code>packages</code> when the R script
loads <code>reticulate</code>.</p>
<p>For Quarto, put Python metadata under the document&rsquo;s <code>ir:</code> key. A knitr
document that uses reticulate can mix R and Python requirements:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">My report</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">packages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">reticulate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">python-packages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pandas</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">exclude-newer</span><span class="p">:</span><span class="w"> </span><span class="ld">2025-01-01</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></div></div>
<p>For a Quarto document that uses the Jupyter engine, the <code>ir:</code> metadata
can contain only Python requirements:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">My notebook</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jupyter</span><span class="p">:</span><span class="w"> </span><span class="l">python3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">python-packages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">matplotlib</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pandas</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.11&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></div></div>
<p>For Quarto renders, <code>ir</code> injects <code>jupyter</code> into the Python environment,
passes the resolved interpreter to Quarto with <code>QUARTO_PYTHON</code>, and also
sets <code>RETICULATE_PYTHON</code> so documents that use reticulate see the same
interpreter. If the document uses the knitr engine and contains Python
chunks, <code>ir</code> automatically adds <code>reticulate</code> to the R package manifest.</p>
<p>When Python metadata is present, <code>exclude-newer</code> is also used for Python
environment resolution unless <code>python-exclude-newer</code> is set. Use
<code>python-exclude-newer</code> when Python packages should use a different
snapshot date from R packages.</p>
<h2 id="install-ir">Install <code>ir</code>
</h2>
<p>Install a pre-built binary on Linux or macOS:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> curl -fsSL https://raw.githubusercontent.com/r-lib/ir/main/scripts/install.sh <span class="p">|</span> sh
</span></span></code></pre></div></div>
<p>Install on Windows PowerShell:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> irm https://raw.githubusercontent.com/r-lib/ir/main/scripts/install.ps1 <span class="p">|</span> iex
</span></span></code></pre></div></div>
<p>The installers download the latest GitHub release and install both <code>ir</code>
and <code>rx</code>. You can also download release artifacts from the
<a href="https://github.com/r-lib/ir/releases" target="_blank" rel="noopener">GitHub releases</a> page or with
<code>gh release download</code>.</p>
<p>You will also need <code>R</code> / <code>Rscript</code>; <code>rig</code> is required when selecting R
by version or by date-only <code>exclude-newer</code>, and Quarto is required when
rendering Quarto sources.</p>
<h2 id="learn-more">Learn more
</h2>
<p>The project is open source under the MIT license. To get started:</p>
<ul>
<li>Read the documentation: <a href="https://r-lib.github.io/ir/" target="_blank" rel="noopener">https://r-lib.github.io/ir/</a></li>
<li>Browse the source: <a href="https://github.com/r-lib/ir" target="_blank" rel="noopener">https://github.com/r-lib/ir</a></li>
<li>Open an issue: <a href="https://github.com/r-lib/ir/issues" target="_blank" rel="noopener">https://github.com/r-lib/ir/issues</a></li>
</ul>
<p>This is a first public release, and feedback is especially useful now.
If you try <code>ir</code> on your own scripts or Quarto documents, we would like
to hear which workflows feel natural, where the metadata model needs
more room, and which command-line edges still need smoothing.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-23_ir-0-1-0/terrarium.png" length="2120322" type="image/png" />
    </item>
    <item>
      <title>AI Newsletter: LLMs often miss subtle visual artifacts in data visualizations</title>
      <link>https://opensource.posit.co/blog/2026-07-17_ai-newsletter/</link>
      <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-17_ai-newsletter/</guid>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<p>Imagine that you receive some patient data and load it into R or Python for the first time. You make a couple plots to get a sense of the data before coming across this one:</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/artifact-plot-1.png" data-fig-align="center" width="768" />
<p>Huh. It mostly looks normal, except there&rsquo;s a few points perfectly aligned with what looks to be a &ldquo;fitted&rdquo; line. You dig into it a bit more, and realize that the rows from one study site have their cholesterol values imputed. You set them to <code>NA</code> and go along your way.</p>
<p>Would today&rsquo;s frontier LLMs catch such an oddity? We designed <a href="https://github.com/posit-dev/bluffbench2" target="_blank" rel="noopener">an LLM evaluation</a> to help us answer this question. As it turns out, LLMs mostly miss these sorts of artifacts:</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/results-plot-1.png" data-fig-align="center" data-fig-alt="A bar plot showing scores for several frontier models. The two leaders, Gemini 3.5 Flash and Claude Fable 5, score in the mid teens. Models from OpenAI cluster at the bottom, never eclipsing 10%." width="768" />
<p>During exploratory or open-ended data analysis, Posit assistant <a href="https://opensource.posit.co/blog/2026-06-08_comparing-posit-assistant-and-claude-code/#specialized-data-analysis-capabilities" target="_blank" rel="noopener">&ldquo;only runs a few bits of code at a time, then summarizes what it found and suggests next steps&rdquo;</a>. This is motivated by our stance that, for now, a data scientist should mostly keep pace with and understand what the agent is doing when analyzing data. This stance was initially informed by our observation that last year&rsquo;s frontier models <a href="https://posit.co/blog/introducing-bluffbench" target="_blank" rel="noopener">tended to see what they expected to see</a> when visualizing data. While <a href="https://opensource.posit.co/blog/2026-06-19_ai-newsletter/" target="_blank" rel="noopener">LLMs have since become much better at interpreting counterintuitive plots</a>, bluffbench2 shows they still lag behind human data scientists in interpreting data visualizations. As such, we are still cautious on the prospect of highly autonomous data agents.</p>
<h2 id="how-the-eval-works">How the eval works
</h2>
<p>The eval harness is a relatively generic coding agent harness, similar to that of Claude Code or Posit Assistant. The agent has a tool to run R code in a persistent REPL and some vague prompting about data analysis:</p>
<blockquote>
<p>You are an AI assistant embedded in the user&rsquo;s data science IDE. You can read and modify files in the user&rsquo;s workspace and execute R code in their active session, including rendering plots. Prioritize correctness and clear communication&hellip;</p>
</blockquote>
<p>In each sample, the agent first carries out a few &ldquo;lull&rdquo; turns, making a couple plots and tables unrelated to the eval. Short user messages like &ldquo;load in the csv in this folder&rdquo; are decorated with &ldquo;System Reminders&rdquo; and other noise like that injected by popular agent harnesses.</p>
<div style="display: flex; flex-direction: column; gap: 8px; padding: 20px; max-width: 100%; margin: 20px auto;">
<div style="align-self: flex-end; background-color: #e8f3fc; padding: 12px 18px; border-radius: 18px 18px 4px 18px; max-width: 70%;">
take a look at <code>dat</code> in my env
</div>
<div style="align-self: flex-start; background-color: white; padding: 12px 18px; border-radius: 18px 18px 18px 4px; max-width: 70%; border: 1px solid #e0e0e0;">
<em>Tool: Run R code</em>
</div>
<div style="align-self: flex-end; background-color: #e8f3fc; padding: 12px 18px; border-radius: 18px 18px 4px 18px; max-width: 70%;">
<em>Tool result</em>
</div>
<div style="align-self: flex-start; background-color: white; padding: 12px 18px; border-radius: 18px 18px 18px 4px; max-width: 70%; border: 1px solid #e0e0e0;">
Looks like <code>dat</code> is a data frame of...
</div>
<div style="align-self: flex-end; background-color: #e8f3fc; padding: 12px 18px; border-radius: 18px 18px 4px 18px; max-width: 70%;">
<span style="display: block; margin-bottom: 8px; font-family: monospace; font-size: 0.8em; opacity: 0.55;">&lt;system-reminder&gt;<br>Your to-do list is currently empty. If you are working on tasks that would benefit from tracking progress, consider creating to-dos. This is just a gentle reminder - ignore if not applicable.<br>&lt;/system-reminder&gt;</span>
summarize <code>$cholesterol</code>
</div>
<div style="align-self: flex-start; background-color: white; padding: 12px 18px; border-radius: 18px 18px 18px 4px; max-width: 70%; border: 1px solid #e0e0e0;">
<em>Tool: Run R code</em>
</div>
</div>
<p>After a few turns, the agent is asked to produce a data visualization that includes a subtle visual artifact that could feasibly result from a real data-generating process. The artifacts span a range of realistic data quality issues: stuck sensors, bad joins, points imputed onto a line, swapped columns, pseudoreplication, differing units, etc.</p>
<div style="display: flex; flex-direction: column; gap: 8px; padding: 20px; max-width: 100%; margin: 20px auto;">
<div style="align-self: flex-end; background-color: #e8f3fc; padding: 12px 18px; border-radius: 18px 18px 4px 18px; max-width: 70%;">
plot bmi vs cholesterol
</div>
<div style="align-self: flex-start; background-color: white; padding: 12px 18px; border-radius: 18px 18px 18px 4px; max-width: 70%; border: 1px solid #e0e0e0;">
<em>Tool: Run R code</em>
</div>
<div style="align-self: flex-end; background-color: #e8f3fc; padding: 12px 18px; border-radius: 18px 18px 4px 18px; max-width: 70%;">
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/images/labs-thumb.png" width="220" style="border-radius: 12px; display: block;">
</div>
</div>
<p>If the agent mentions the artifact in its follow-up response, it receives a full point. If the agent does not mention the artifact, it can also receive a half point by mentioning it in response to a follow-up user message along the lines of &ldquo;what do you see in the plot?&rdquo; If the agent never mentions the artifact, it is graded as incorrect.</p>
<h2 id="designing-the-eval">Designing the eval
</h2>
<p>Once we understood the mechanism behind bluffbench, implementing the eval was relatively straightforward. bluffbench demonstrates the degree to which an LLM will ignore evidence shown in a plot in favor of its expectations. So, to implement a given sample, we&rsquo;d just think of some situation that would elicit a strong prior and then subvert it. For example, a dataset called <code>doug_firs</code> with variables <code>height</code> and <code>circumference</code>; one might expect that, as height increases, so does circumference. So, instead, we did a transformation under the hood that made the relationship parabolic.</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/trees-plot-1.png" data-fig-align="center" data-fig-alt="Two scatterplots side by side, both with circumference on the x axis and height on the y axis. The left, labeled &#39;Original Plot&#39;, shows height rising with circumference, a positive trend. The right, labeled &#39;Tampered Plot&#39;, shows height rising then falling as circumference increases, an inverted-U shape." width="768" />
<p>A year ago, triggering this prior was enough to frequently &rsquo;trick&rsquo; the current frontier LLMs.</p>
<p>Slipping a plotted artifact past today&rsquo;s LLMs is much harder. Any human could ace bluffbench, but only an attentive data analyst would excel at bluffbench2.</p>
<p>In our early work on a successor to bluffbench, we started off with trying to elicit priors in the same way as bluffbench did, but in more realistic, longer-context scenarios. We were surprised to find that the same mechanism broadly doesn&rsquo;t seem to trick today&rsquo;s models even in these more realistic settings.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> We then tried a &lsquo;reverse bluffbench&rsquo;, where we let the model being evaluated in on the trick, asking it to carry out the transformation itself and then look at the plotted result which was tampered with to show the original relationship. We anticipated that this stronger prior (&ldquo;I did a thing with an obvious effect&rdquo;) might cause the models to miss the (re)manipulation, but models reliably noted that the plot looked as if it hadn&rsquo;t been manipulated.</p>
<p>As such, there isn&rsquo;t a similar &rsquo;trick&rsquo; in bluffbench2 per se. The transcripts read like relatively normal data analysis sessions and the plotted artifacts are designed to plausibly result from real data-generating processes. Instead, the eval elicits 1) the &lsquo;shape&rsquo; of LLMs&rsquo; vision being different than humans&rsquo; and 2) the model&rsquo;s tendencies to perform progress, simulating a data analysis moving along smoothly.</p>
<p>Today&rsquo;s frontier models are in the mid-teens at best; the top scores belong to Claude Fable 5 and Gemini 3.5 Flash at 16%. That said, we&rsquo;d caution folks from interpreting the current scores on this eval as &lsquo;LLMs don&rsquo;t see plots well.&rsquo; The plotted artifacts are actually quite subtle, and when they&rsquo;re made even a bit more marked, models tend to call them out consistently.</p>
<p>For example, the previous version of the scatterplot had a slightly more dense cluster of points:</p>
<div class="panel-tabset">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Previous</a></li>
<li><a href="#tabset-1-2">Current</a></li>
</ul>
<div id="tabset-1-1">
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/previous-labs-plot-1.png" data-fig-align="center" data-fig-alt="A scatterplot of BMI versus cholesterol with a dense run of roughly fifty points falling exactly on a straight line through the noisy cloud." width="768" />
</div>
<div id="tabset-1-2">
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/current-labs-plot-1.png" data-fig-align="center" data-fig-alt="A scatterplot of BMI versus cholesterol with a sparse run of about thirty points falling exactly on a straight line through the noisy cloud, subtler than the previous version." width="768" />
</div>
</div>
<p>Opus 4.8 (medium) consistently got this sample right in the previous iteration.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p>The fact that this was the case&mdash;that models would call out more marked artifacts reliably&mdash;gave us confidence that our grading setup was reasonable. In other words, it does indeed seem like models are struggling with these tasks because their vision is not capable enough to &lsquo;see&rsquo; the plotted artifact rather than a behavioral tendency to not mention those artifacts when they do see them.</p>
</div>
</div>
<h2 id="exploring-the-evals-results">Exploring the eval&rsquo;s results
</h2>
<p>At least for now, there&rsquo;s a loosely linear relationship between the cost to run the eval and the resulting score:</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/cost-plot-1.png" data-fig-align="center" data-fig-alt="A scatterplot of score against total cost for each frontier model, colored by lab. The two leaders, Gemini 3.5 Flash and Claude Fable 5, sit highest at around the mid teens, while the OpenAI models sit low regardless of cost. Higher spend does not buy a higher score." width="768" />
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p>Given that Gemini 3.5 Flash is so much cheaper than Claude Fable 5 per-token ($1.50/$9 per mTok I/O vs. $10/$50), it&rsquo;s surprising that the eval was so expensive to run for Gemini 3.5 Flash. This is primarily driven by cache (in)efficiency; the harness is implemented against Gemini&rsquo;s <code>generateContent</code> API, which makes it difficult to make use of discounted cached input pricing compared to Anthropic and OpenAI&rsquo;s APIs. Implementing and switching to Gemini&rsquo;s newer Interactions API would push the Flash 3.5 point to the left.</p>
</div>
</div>
<p>One of the most interesting learnings from examining the logs is a behavioral one. Even though we never request that LLMs introduce modeled results to plots, like fitted lines and confidence intervals with <code>geom_smooth(method = &quot;lm&quot;, se = TRUE)</code>, they sometimes do so anyway. For example:</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/smooth-example-1.png" data-fig-align="center" data-fig-alt="The BMI versus cholesterol scatterplot with a straight fitted line and a shaded confidence-interval ribbon laid over the points, an overlay the model added on its own." width="768" />
<p>In general, adding modeled results to data visualizations without first looking at data is bad practice; it makes it hard to see the data itself. In the eval, adding a modeled result like this seems to substantially lower the chances that the model will notice the plotted artifact:</p>
<img src="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/smooth-plot-1.png" data-fig-align="center" data-fig-alt="A dumbbell plot, one row per model, comparing accuracy on artifact plots the model drew with a geom_smooth() overlay versus without. For nearly every model the &#39;with overlay&#39; point sits well to the left of the &#39;without&#39; point; Claude Fable 5 falls from about a quarter correct to zero, and Gemini 3.5 Flash from about a quarter to under a tenth." width="768" />
<h2 id="more-bluffbench">More bluffbench
</h2>
<p>If you&rsquo;d like to learn more about the bluffbench set of evals, take a look at these past posts:</p>
<ul>
<li><a href="https://posit.co/blog/introducing-bluffbench" target="_blank" rel="noopener"><strong>Introducing bluffbench</strong></a>: Writeup of the the original eval.</li>
<li><a href="https://posit.co/blog/llm-plot-interpretation" target="_blank" rel="noopener"><strong>LLMs interpret plots well, until expectations interfere</strong></a>: In-depth post on why models at the time didn&rsquo;t perform well on bluffbench, as well as various interventions we tried to improve performance.</li>
<li><a href="https://opensource.posit.co/blog/2026-06-19_ai-newsletter/" target="_blank" rel="noopener"><strong>LLMs are getting much better at interpreting counterintuitive plots</strong></a>: In spring 2026, bluffbench scores suddenly jumped as models improved.</li>
<li><a href="https://skaltman.github.io/scipy-2026/" target="_blank" rel="noopener"><em><strong>It&rsquo;s (still) very bad to be wrong</strong></em></a>: Slides from our recent SciPy 2026 talk on building agents for correct, transparent, and reproducible data analysis in light of the bluffbench results.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>This somewhat alleviated the fear that models had just memorized the bluffbench eval setup, mentioned in <a href="https://opensource.posit.co/blog/2026-06-19_ai-newsletter/" target="_blank" rel="noopener">our previous post</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/images/hero.png" length="114420" type="image/png" />
    </item>
    <item>
      <title>Tips for managing your Python &amp; R environments in Positron</title>
      <link>https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/</link>
      <pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/</guid>
      <dc:creator>Cindy Tong</dc:creator>
      <dc:creator>Brice Stacey</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is the Posit next-generation IDE for data science. Positron is an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>If you work across Python and R or juggle multiple projects with different dependency needs, environment management is probably one of the most challenging parts of your workflow. <a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a> comes with several out-of-the-box ways to help simplify environment management. Here are a few tips to streamline your workflow.</p>
<h2 id="1-understand-how-positron-discovers-your-environment">1: Understand how Positron discovers your environment
</h2>
<p>Positron does not just look at your system PATH. For Python, it actively discovers venv, uv, pyenv, and conda environments. If Positron is not seeing your project&rsquo;s virtual environment, you can add custom search locations via the <a href="positron://settings/python.interpreters.include"><code>python.interpreters.include</code></a> setting, or trigger a manual rescan with <em>Interpreter: Discover All Interpreters</em>. Learn more about <a href="https://positron.posit.co/python-installations.html#python-installation-discovery" target="_blank" rel="noopener">Python discovery in the Positron documentation</a>.</p>
<p>For R, discovery works differently. Positron consults various sources to build the list of R interpreters. These include your PATH, R root folders based on specific operating systems, well-known executable locations, and on Windows the registry. You can customize your R discovery through a few settings including <a href="positron://settings/positron.r.customRootFolders"><code>positron.r.customRootFolders</code></a> and <a href="positron://settings/positron.r.customBinaries"><code>positron.r.customBinaries</code></a>. Learn more about <a href="https://positron.posit.co/r-installations.html#customizing-r-discovery" target="_blank" rel="noopener">R discovery in the Positron documentation</a>.</p>
<h2 id="2-use-the-interpreter-selector">2. Use the Interpreter Selector
</h2>
<p>To begin your first session, click &ldquo;Start Session&rdquo; in the top right corner and select your preferred R or Python interpreter. Positron can run multiple R and Python interpreter sessions at once, but only one is ever the active session at a given moment. The Interpreter Selector always shows you the active session and its status (idle, busy, or shut down) and you can use it to switch between or start additional sessions.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/active-interpreter-session.png" width="100%" data-fig-align="center" data-fig-alt="Use the Interpreter Selector to change your active session" />
<h2 id="3-install-python-with-uv">3. Install Python with uv
</h2>
<p>If Positron does not find a usable Python on your machine, it will offer to install Python via uv to help streamline your setup. If you prefer to manage the installation yourself, you can disable uv with the <a href="positron://settings/python.allowUvPythonInstall"><code>python.allowUvPythonInstall</code></a> setting. Check out our <a href="https://opensource.posit.co/blog/2026-07-08_positron-uv/" target="_blank" rel="noopener">blog post exploring on-demand Python installation in Positron</a> or explore configurations in our <a href="https://positron.posit.co/python-installations.html#troubleshooting" target="_blank" rel="noopener">Python installation documentation</a>.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/install-python-with-uv.png" width="100%" data-fig-align="center" data-fig-alt="Install Python via uv" />
<h2 id="4-manage-packages-with-the-packages-pane">4. Manage packages with the Packages Pane
</h2>
<p>The Packages Pane in Positron lets you manage the packages installed in your active session. Whether you use pip, uv, conda, pak, base R, or renv, you can browse installed packages and search package repositories. You can also track outdated packages and install, update, or uninstall packages without leaving Positron or writing any code.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/packages-pane.gif" width="100%"  data-fig-align="center" data-fig-alt="Manage Python and R packages in the Packages Pane" />
<h2 id="5-view-package-documentation">5. View package documentation
</h2>
<p>If you need to learn more about a specific package, the Packages Pane has buttons to navigate to the source documentation or link to the package&rsquo;s website. You can also pull up the <a href="https://positron.posit.co/help-pane.html" target="_blank" rel="noopener">Help Pane</a> for any reference in the Console using the <code>?</code> operator, including both packages and functions.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/packages-pane-help.png" width="100%" data-fig-align="center" data-fig-alt="View source documentation for specific packages in the Help Pane" />
<h2 id="6-start-projects-with-new-folder-from-template">6. Start projects with New Folder from Template
</h2>
<p>The New Folder from Template flow helps you start new projects faster. Instead of running multiple setup commands you can make a few selections and Positron helps you set up an environment directory, version control, directory structure, and an interpreter instance. Learn more about the <a href="https://positron.posit.co/folder-templates.html" target="_blank" rel="noopener">Python and R templates</a> available in the documentation.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/new-folder-from-template.png" width="100%" data-fig-align="center" data-fig-alt="New Folder from Template helps you start new projects faster with ready to use environments" />
<h2 id="7-make-use-of-the-command-palette">7. Make use of the Command Palette
</h2>
<p>We recommend taking advantage of the following commands when managing your environments:</p>
<ul>
<li><em>Interpreter: Discover All Interpreters</em>: Find new interpreters</li>
<li><em>Interpreter: Select Session</em>: Select a running interpreter session</li>
<li><em>Python: Create Environment</em>: Create a new virtual environment</li>
</ul>
<h2 id="8-let-posit-assistant-help-manage-your-environment">8. Let Posit Assistant help manage your environment
</h2>
<p><a href="https://assistant.posit.co/docs/features/context-management/" target="_blank" rel="noopener">Posit Assistant</a> has knowledge of your R and Python session including the language, version, names, and types of variables in your environment. You can prompt Posit Assistant to set up environments and troubleshoot issues you run into.</p>
<img src="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/assistant-installed-packages.png" width="100%" data-fig-align="center" data-fig-alt="Posit Assistant in Positron showing a summary of packages installed in the user's Python environment, organized by category." />
<p>Have an idea for how we can improve environment management in Positron? We would love to hear from you in a <a href="https://github.com/posit-dev/positron/discussions" target="_blank" rel="noopener">discussion on GitHub</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/featured.svg" length="75593" type="image/svg&#43;xml" />
    </item>
    <item>
      <title>httr2 1.3.0</title>
      <link>https://opensource.posit.co/blog/2026-07-14_httr2-1-3-0/</link>
      <pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-14_httr2-1-3-0/</guid>
      <dc:creator>Hadley Wickham</dc:creator><description><![CDATA[<p>We&rsquo;re chuffed to announce the release of <a href="https://httr2.r-lib.org" target="_blank" rel="noopener">httr2</a> 1.3.0. httr2 makes it easy to work with web APIs from R, providing a pipeable interface for building requests and a suite of tools for processing the responses.</p>
<p>Get it now:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># install.packages(&#34;pak&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="s">&#34;httr2&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The headline changes in this release all relate to OAuth token caching. They happen behind the scenes, so you shouldn&rsquo;t need to change any code, but you&rsquo;ll likely need to re-authenticate once after upgrading. The rest of this post explains why, and rounds up a handful of smaller features that landed in recent patch releases. See a full list of changes in the <a href="https://github.com/r-lib/httr2/releases/tag/v1.3.0" target="_blank" rel="noopener">release notes</a>.</p>
<h2 id="oauth-token-caching">OAuth token caching
</h2>
<p>First, a bit of background. When you authenticate with an API using OAuth, the API typically returns a refresh token that you can reuse for a set period. That means you don&rsquo;t have to log in every single time you use the API, which makes for a much nicer experience. To reuse a token across R sessions, however, httr2 has to write it to disk, and that creates a risk: anyone who can read the token could use it to access the API as you.</p>
<p>httr2 takes every precaution it can to keep these tokens safe. They&rsquo;re stored in a user-local cache directory, which is usually excluded from backups and, because it lives outside your working directory, carries no risk of being accidentally committed to git. On top of that, the tokens are encrypted so that only httr2 can read them.</p>
<p>Each token&rsquo;s path on disk is derived from a hash computed with <code>rlang::hash()</code>. We recently discovered a bug in that hashing code, and fixing it changes every hash. As a result, httr2 can no longer find your existing cached tokens, so you&rsquo;ll need to re-authenticate once after upgrading.</p>
<p>Since that change already invalidates your cached tokens, we took the opportunity to move where they&rsquo;re stored. Previously we used the rappdirs package to locate the cache directory, but since R 4.0.0 there&rsquo;s been a better built-in alternative: <code>tools::R_user_dir(&quot;httr2&quot;, which = &quot;cache&quot;)</code>. We&rsquo;ve switched to that, which makes httr2&rsquo;s cache location consistent with other packages and removes the rappdirs dependency.</p>
<p>Making these changes surfaced a long-standing bug. Every time httr2 loads, it&rsquo;s supposed to automatically delete any cached tokens older than 30 days, so that stale tokens don&rsquo;t linger indefinitely. Unfortunately, that pruning never actually worked. Now it does, so old tokens will be cleaned up once they pass 30 days, regardless of which cache directory or hashing algorithm produced them. This is good security practice: the longer a credential sits on disk, the more opportunities there are for it to leak, so removing tokens you&rsquo;re no longer using shrinks that window of exposure. If you&rsquo;d like to prune every cached token yourself, you can call <code>oauth_cache_prune(max_age_days = 0)</code>.</p>
<h2 id="other-features">Other features
</h2>
<p>Alongside the token caching work, this release also rolls up a few smaller features that first appeared in recent patch releases:</p>
<ul>
<li>
<p><strong>Faster streaming</strong> (1.2.3): <code>resp_stream_lines()</code>, <code>resp_stream_sse()</code>, and <code>resp_stream_aws()</code> now decode whole chunks at a time and hold the results in a queue, instead of rescanning and recopying the buffer for every line or event. This makes memory use and run time scale linearly rather than quadratically, so large streams are dramatically faster (e.g. reading a 1 MB response of short lines is now around 200x faster and uses around 180x less memory). <code>resp_stream_sse()</code> is used heavily by <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>, so this should make your LLM streaming feel a little zippier.</p>
</li>
<li>
<p><strong>OAuth server metadata discovery</strong> (1.2.3): the new <code>oauth_server_metadata()</code> discovers an OAuth/OIDC issuer&rsquo;s endpoints from its <code>.well-known</code> document, and <code>oauth_client()</code> gains a <code>metadata</code> argument so you can supply all of those endpoints at once.</p>
</li>
<li>
<p><strong>OpenTelemetry tracing</strong> (1.2.2): httr2 now emits OpenTelemetry traces for all requests when tracing is enabled. This requires the otelsdk package and is part of our cross-package work to improve the <a href="https://opensource.posit.co/blog/2026-05-07_opentelemetry/" target="_blank" rel="noopener">observability of R packages</a>.</p>
</li>
<li>
<p><strong><code>httr2_translate()</code></strong> (1.2.3): this new function translates an httr2 request into the equivalent curl command. It&rsquo;s handy for debugging, or for creating reprexes to share with people who don&rsquo;t use R.</p>
</li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-14_httr2-1-3-0/featured.jpg" length="312741" type="image/jpeg" />
    </item>
    <item>
      <title>useR! 2026: CRDTs, community, and contributing back to R</title>
      <link>https://opensource.posit.co/blog/2026-07-14_user-2026-warsaw/</link>
      <pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-14_user-2026-warsaw/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<p>Although the title of this post starts with &ldquo;CRDTs&rdquo; (for maximal alliterative effect), it is in fact the other two (community and contributing back) which are way more important to me. Sometimes it takes a conference, a substantial chunk of time away from the desk, to step back and re-calibrate our perspectives: to really appreciate the sheer breadth of <em>good work</em> taking place within the community.</p>
<p>And this community is ours. From joining my very first useR! online in 2022, through to Salzburg in 2024 - SatRdays London in between, the R Project Sprint in Warwick (UK) in 2023, the R Dev Days since, and of course posit::conf(2024) Seattle, posit::conf(2025) Atlanta, and Japan.R 2025&hellip; I may not have been part of this community for the most number of years, but I&rsquo;ve certainly appreciated its warmth and openness.</p>
<p>And so coming back to a European useR! was exciting.</p>
<h2 id="distributing-state-crdts-for-real-time-collaboration">Distributing state: CRDTs for real-time collaboration
</h2>
<p>The R community knows how to distribute compute - I&rsquo;ve talked extensively on this subject, mainly surrounding my creation {<a href="https://mirai.r-lib.org/" target="_blank" rel="noopener">mirai</a>}, an async framework that brings high-performance parallel and distributed computing to R. This lets you send tasks just as easily to a Slurm cluster as to processes on your own machine.</p>
<p>My talk this time was going to be about something different, but complementary: distributing state. A CRDT, or Conflict-free Replicated Data Type, is a data structure that facilitates collaboration by having a property called strong eventual consistency. Without going into the details, this allows it to always merge conflict-free, and is the technology behind many collaborative text editors. Automerge, a particular CRDT implementation, is what we&rsquo;re using in Quarto 2 to make it collaborative out-of-the-box.</p>
<p>In my talk, I showed a Shiny app in an R session editing a document on one of our Quarto collaborative sync servers. It used the {<a href="https://posit-dev.github.io/automerge-r/" target="_blank" rel="noopener">automerge</a>} and {<a href="https://posit-dev.github.io/autosync/" target="_blank" rel="noopener">autosync</a>} packages, which we created to let R manipulate these structures and sync them over the network - on a par with reference implementations in JavaScript and Rust. Automerge was created by Ink and Switch, an independent research lab for local-first software, and we&rsquo;re glad to provide a link between the two communities.</p>
<h2 id="distributing-compute-mirai-and-mori-in-the-wild">Distributing compute: mirai and mori in the wild
</h2>
<p>Apart from talking a lot about CRDTs and the three killer features of Quarto 2 (orders of magnitude faster, collaborative editing built-in with live preview, editable in the preview and source views), I also got pulled aside by people in the hallways to talk about {<a href="https://mirai.r-lib.org/" target="_blank" rel="noopener">mirai</a>} and {<a href="https://shikokuchuo.net/mori/" target="_blank" rel="noopener">mori</a>}.</p>
<p>Some of these were people working in the life sciences industry, using these packages for serious scientific research and innovation. This has always been a sector that I&rsquo;ve had immense respect for - from my earliest collaboration with Will Landau (Eli Lilly &amp; Co.). Open source software is often regarded as &lsquo;high-leverage&rsquo; in terms of how often and widely it&rsquo;s used, and when deployed in such high-impact settings, the end goals can be especially motivating and rewarding.</p>
<p>It&rsquo;s been extremely gratifying to see that {<a href="https://mirai.r-lib.org/" target="_blank" rel="noopener">mirai</a>} has percolated throughout the community consciousness. Considering that I was only introducing the package at useR! 2024 in Salzburg, it was nice to be able to introduce <em>myself</em> to people this time simply as &ldquo;the author of mirai&rdquo;. By 2027, I&rsquo;m hoping this will also be possible with mori!</p>
<h2 id="the-best-part-the-people">The best part: the people
</h2>
<p>Given that only a few of my colleagues could make it to useR! this year, I was slightly apprehensive going in. This fear was dispelled at the very first reception event, where I met so many people from past R Dev Days, amongst them Tina Roszos, who simply beamed at me from across the room! R Core also turned out in force and it was nice to meet Peter Dalgaard and Robert Gentleman for the first time, as well as saying hello again to Luke Tierney, Uwe Ligges and everyone who came.</p>
<p>It&rsquo;s always great to catch up with familiar faces - Gergely Daróczi (maintainer of R&rsquo;s Weblate platform), members of the mlr group (Martin Binder, Marc Becker, Maximilian Mücke), and many others. However, some of my best conversations this year were with people I had met for the first time. I was fortunate that for some, this was their first in-person useR!, having been part of the R community for many years.</p>
<p>It was especially nice to be welcomed by entire groups of people, for example the <a href="https://numbat.space" target="_blank" rel="noopener">NUMBATs</a> headed by Prof. Dianne Cook, who was also the opening keynote speaker of the conference. I got to be an honorary NUMBAT for one photo:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-07-14_user-2026-warsaw/images/numbat.jpg"
      alt="The Monash &ldquo;NUMBAT&rdquo; group posing inside a &ldquo;useR! 2026 Warsaw&rdquo; photo-frame prop covered in R-package hex stickers." 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="contributing-back-r-dev-day">Contributing back: R Dev Day
</h2>
<p>R Dev Day followed the conference on the Friday. Thanks again to Heather Turner and Ella Kaye for tirelessly organising these. They are simply great opportunities to contribute back to R&rsquo;s source code itself. I do believe that these events have played an important role in fostering and sustaining our community.</p>
<p>I worked closely with Tymek (Tymoteusz Makowski) on two C-level I/O bugs. Tymek was the only person brave enough to tackle these. This proved to be a fruitful collaboration as we&rsquo;d posted a patch by the end of the day (<a href="https://bugs.r-project.org/show_bug.cgi?id=19101" target="_blank" rel="noopener">PR #19101</a>). This patch has since been accepted into the R source, without modification! It&rsquo;s proved the most productive of any R Dev Day I&rsquo;ve attended thus far.</p>
<p>Special thanks to Mitchell O&rsquo;Hara-Wild for supplying the Dev Day group photograph, which I&rsquo;m using as the banner image for this blog post.</p>
<h2 id="looking-ahead">Looking ahead
</h2>
<p>There are many people I&rsquo;ve not mentioned by name - that&rsquo;s just to prevent this entire post from being a name-drop. I&rsquo;ve enjoyed speaking to every one of you. There is so much talent out there, and I look forward to catching up again and marvelling at all you&rsquo;ve been working on!</p>
<p>See you next at <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">posit::conf(2026)</a> Houston!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-14_user-2026-warsaw/images/hero.jpg" length="575131" type="image/jpeg" />
    </item>
    <item>
      <title>Positron July Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/</link>
      <pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Welcome back to another edition of our monthly Positron updates! Each month we share highlights from our <a href="https://positron.posit.co/release-notes" target="_blank" rel="noopener">latest release</a> and useful resources. <a href="https://opensource.posit.co/blog/2026-06-08_positron-2026-06-release/" target="_blank" rel="noopener">Last release</a> we told you that several major features were on track to leave preview in July, and the time is now here! The new Notebook editor, the Packages pane, and Posit Assistant are all now generally available.</p>
<h2 id="positron-notebook-editor">Positron Notebook Editor
</h2>
<p>Positron&rsquo;s <a href="https://positron.posit.co/positron-notebook-editor" target="_blank" rel="noopener">new Notebook editor</a> is now the default experience for Jupyter (<code>.ipynb</code>) files. This release brings a long list of additions, including split-pane editing, cell tag management, and executing a line or selection within a cell with <kbd>Cmd/Ctrl+Shift+Enter</kbd>. You can export notebooks to Quarto, Python, or R, and inline PDF rendering makes it easier to work with generated output. If you are coming from JupyterLab, you will find familiar keyboard shortcuts, and we have improved output fidelity for Mermaid diagrams, htmlwidgets, and ipywidgets.</p>
<img src="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/positron-notebook-editor.png" data-fig-align="center" data-fig-alt="The Notebook editor in Positron, showing a Jupyter notebook with a Mermaid diagram." />
<p>Posit has built on and for the Jupyter ecosystem for years now, and we are excited to have recently <a href="https://opensource.posit.co/blog/2026-06-25_posit-joins-jupyter-foundation/" target="_blank" rel="noopener">joined the Jupyter Foundation</a>.</p>
<h2 id="packages-pane">Packages pane
</h2>
<p>The <a href="https://positron.posit.co/packages-pane" target="_blank" rel="noopener">Packages pane</a> has also come out of preview. It gives you a live view of the R and Python packages installed in your active session, so you can search, install, update, and remove packages, and jump to their documentation, without leaving Positron.</p>
<img src="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/packages-pane.gif" data-fig-align="center" data-fig-alt="The Packages pane in Positron, showing installed Python packages with a detail editor." />
<p>This release also makes the pane more capable. Clicking a package opens a detail editor with its overview, metadata, and actions, similar to the Extensions pane. Installing a package searches results as you type, and each row has a button that opens the package&rsquo;s website. Update indicators appear immediately on a new session from a cached snapshot, and <strong>Update All Packages</strong> reports exactly what changed. Package operations keep your environment consistent too, resolving against a workspace <code>requirements.txt</code> for Python and updating the <code>renv.lock</code> snapshot for R.</p>
<h2 id="posit-assistant">Posit Assistant
</h2>
<p><a href="https://pos.it/assistant" target="_blank" rel="noopener">Posit Assistant</a>, our unified, data-science-focused approach to AI assistance, is now out of preview and generally available; Posit Assistant fully replaces the legacy Positron Assistant. We know the names are similar, and Joe Cheng <a href="https://opensource.posit.co/blog/2026-06-11_history-of-posit-data-science-agents/" target="_blank" rel="noopener">recently walked through a bit of the story behind how our AI agent tools have evolved</a>.</p>
<p>This release also gives you finer control over AI in Positron. The new <a href="positron://settings/ai.enabled"><code>ai.enabled</code></a> setting turns off every Positron AI feature at once, and administrators can enforce it; <a href="positron://settings/notebook.ai.enabled"><code>notebook.ai.enabled</code></a> does the same for notebooks specifically. The set of language model providers keeps growing, with DeepSeek joining as an experimental provider and Microsoft Foundry reaching general availability. The configuration modal now shows every provider by default with preview and experimental badges.</p>
<h2 id="data-explorer">Data Explorer
</h2>
<p>The <a href="https://positron.posit.co/data-explorer" target="_blank" rel="noopener">Data Explorer</a> can now open Excel workbooks directly, with no code required, without first needing to load it via Python or R. Sort, filter, and profile columns, switch between worksheets, and toggle whether the first row holds column names. An <strong>Open in Excel</strong> button opens the workbook in your native spreadsheet application.</p>
<p>This release broadens what you can open in the Data Explorer overall. Backed by a native DuckDB engine, it now also previews compressed CSV, TSV, and Parquet files. Also, a new <strong>Open in Data Explorer</strong> code action, from the editor lightbulb or <kbd>Cmd+.</kbd>, opens the data frame under your cursor in R, Python, and Quarto files, so you can jump straight from your code to exploring your data.</p>
<img src="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/open-in-data-explorer-code-action.gif" data-fig-align="center" data-fig-alt="The Open in Data Explorer code action in Positron, showing a lightbulb menu in a Python file with the option to open a data frame in the Data Explorer." />
<h2 id="r-language-intelligence">R language intelligence
</h2>
<p>Last release we introduced within-file symbol resolution for R; this release expands it across files. Go to Definition, Find References, and Rename Symbol now work across the packages and scripts in your workspace, and diagnostics and workspace symbols react to external file changes so your language intelligence stays in sync as your project evolves.</p>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<ul>
<li>New in preview this release, Data Connections lets you browse the schemas, tables, views, and indexes of a database, open tables in the Data Explorer, and generate connection code. It currently supports DuckDB, PostgreSQL, and SQLite. Learn how to try it out and tell us what you think in the <a href="https://github.com/posit-dev/positron/discussions/14571" target="_blank" rel="noopener">Data Connections discussion post</a>: which databases and warehouses you need, whether the connection setup is clear, and anything confusing, missing, or broken.</li>
<li>We are looking forward to posit::conf(2026) in September, where our team will have several sessions on Positron. <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">Register now</a> to join us in person in Houston or virtually from anywhere in the world.</li>
</ul>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> to try out the new features and improvements in this release!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/featured.svg" length="88493" type="image/svg&#43;xml" />
    </item>
    <item>
      <title>Positron &#43; uv: Python setup in one click</title>
      <link>https://opensource.posit.co/blog/2026-07-08_positron-uv/</link>
      <pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-08_positron-uv/</guid>
      <dc:creator>Isabel Zimmerman</dc:creator><description><![CDATA[<p>The Python community has a long-standing joke about how hard it can be to get Python running on your machine.
The options can feel overwhelming, and the post-install result often looks like this:</p>
<img src="https://imgs.xkcd.com/comics/python_environment.png" data-fig-align="center" data-fig-alt="The xkcd 'Python Environment' comic showing a tangled web of arrows between dozens of conflicting Python installations on one machine" />
<p>If you&rsquo;re new to Python, Positron has a few built-in workflows to help you skip that installation pain entirely.
Most of it is powered by <a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener">uv, created by Astral</a>.
We chose uv because it&rsquo;s fast and has quickly become widely adopted across the Python community.
Behind the scenes, uv handles the heavy lifting, downloading Python versions and building environments.
Positron wraps that power in a friendly interface, so you never have to remember a command.</p>
<h2 id="from-zero-to-python-in-one-click">From zero to Python in one click
</h2>
<p>If you don&rsquo;t have a suitable Python available, Positron will offer to install it for you.
When you go to start a Python runtime through the <strong>Start Session</strong> button, you&rsquo;ll see an option for <strong>+ Install Python via uv</strong>.</p>
<img src="https://opensource.posit.co/blog/2026-07-08_positron-uv/select-interpreter.png" data-fig-align="center" data-fig-alt="Positron's 'Start New Console Session' picker listing installed Python and R interpreters, with a highlighted '+ Install Python via uv' option at the bottom" />
<p>When you choose it, Positron will ask to install uv for you if you don&rsquo;t already have it.
Then it shows you the supported Python versions (currently 3.9 through 3.14) and installs whichever one you pick.
If you have a folder open, it will also offer to create a virtual environment for the project and start a Python console session using that environment.
Once you have a Python available, this option disappears from the session picker.</p>
<p>If you&rsquo;d rather not see the prompt to install Python, you can turn it off with the <a href="positron://settings/python.allowUvPythonInstall"><code>python.allowUvPythonInstall</code></a> setting (enabled by default).</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Why do I see this when I already have Python on my machine?</span>
</div>
<div class="callout-body">
<p>If you only have system Pythons available, you&rsquo;ll still see the <strong>+ Install Python via uv</strong> option.
<a href="https://pydevtools.com/handbook/explanation/why-should-i-avoid-system-python/" target="_blank" rel="noopener">Using system Python tends to cause problems down the road</a>, so Positron nudges you toward a managed Python and virtual environment instead.</p>
</div>
</div>
<h2 id="on-demand-python-installation">On-demand Python installation
</h2>
<p>You don&rsquo;t have to wait for Positron to ask, either.
Open the command palette with <kbd>Cmd/Ctrl+Shift+P</kbd> and run <em>Python: Install Python via uv</em> any time you&rsquo;d like another version.
This command will download uv if needed, show you the available Python versions, and optionally offer to create a virtual environment.</p>
<img src="https://opensource.posit.co/blog/2026-07-08_positron-uv/cmd-uv.png" data-fig-align="center" data-fig-alt="Positron's command palette with 'python install via' typed, showing the 'Python: Install Python via uv' command" />
<p>Since it runs on demand, it&rsquo;s a good way to add another interpreter to a project you&rsquo;ve already set up.
And because it follows the same flow every time, it works well for teaching, since everyone ends up in the same place, the same way.</p>
<h2 id="automatic-project-setup-support">Automatic project setup support
</h2>
<p>Setting up a virtual environment isn&rsquo;t limited to that first run.
If Positron finds a <code>pyproject.toml</code> or <code>requirements.txt</code> in a project that doesn&rsquo;t have a virtual environment yet, it&rsquo;ll offer to create one with uv and install your dependencies.
This comes in handy when you&rsquo;ve cloned a colleague&rsquo;s project and want to get up and running without extra setup.</p>
<img src="https://opensource.posit.co/blog/2026-07-08_positron-uv/requirements.png" data-fig-align="center" data-fig-alt="A Positron notification reading 'This project has requirements.txt but no virtual environment. Use uv to create a .venv and install these requirements?' with Yes, Not now, and Don't show again buttons" />
<p>If there&rsquo;s a single requirements source, like a lone <code>requirements.txt</code> or <code>pyproject.toml</code>, Positron will prompt you to create a <code>.venv</code> and install everything right away.
And if there are several sources, you can choose which files to include in your installation.</p>
<p>Positron also knows when to stay out of your way.
If you already have a local virtual environment, or if you use non-uv files like <code>environment.yml</code>, <code>Pipfile</code>, or <code>poetry.lock</code>, you won&rsquo;t get a prompt at all.</p>
<h2 id="try-it-yourself">Try it yourself
</h2>
<p>Setting up Python doesn&rsquo;t have to feel like a chore, whether you&rsquo;re starting on a brand new machine, picking up a colleague&rsquo;s project, or getting a room full of students ready to code.</p>
<p>Give it a try in <a href="https://positron.posit.co/download" target="_blank" rel="noopener">Positron&rsquo;s July 2026 release</a> and let us know what you think.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-08_positron-uv/image.png" length="386340" type="image/png" />
    </item>
    <item>
      <title>mcptools 1.0.0</title>
      <link>https://opensource.posit.co/blog/2026-07-06_mcptools-1-0-0/</link>
      <pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-06_mcptools-1-0-0/</guid>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<p>The first major release of <a href="https://posit-dev.github.io/mcptools/" target="_blank" rel="noopener">mcptools</a>, an R SDK for the Model Context Protocol, is now on CRAN! This release includes several notable features: fetching tools from remote authenticated servers, deploying MCP servers on Posit Connect, and support for images and other rich content types.</p>
<p>To install the package, run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mcptools&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>To demo these new features, I&rsquo;ll deploy an R function that returns a picture to an MCP server on Posit Connect. Then, in another R session, I&rsquo;ll connect to that server and ask an ellmer chat to take a look at the picture and tell me what it sees.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mcptools</span><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="images-in-tool-results">Images in tool results
</h2>
<p>mcptools supports &ldquo;both directions&rdquo; of MCP. In one direction, users can deploy R functions as MCP servers. In the other direction, users can fetch tools from third-party MCP servers as R functions. <strong>mcptools now supports both serving and fetching tools that return images.</strong></p>
<p>As an example of a function that returns an image, let&rsquo;s consider a tool <code>fetch_reference_image()</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tools</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">fetch_reference_image</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="kr">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">content_image_url</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;https://simonpcouch.com/blog/2026-04-16-local-agents-2/featured.png&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;fetch_reference_image&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Fetch the reference image.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>Running that function:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tools</span><span class="o">$</span><span class="nf">fetch_reference_image</span><span class="p">()</span></span></span></code></pre></div></div>
<figure>
<img src="https://simonpcouch.com/blog/2026-04-16-local-agents-2/featured.png" alt="A brown and white Border Collie on a deck, looking attentively at the camera, with wire railings and blurred greenery in the background." />
<figcaption aria-hidden="true">A brown and white Border Collie on a deck, looking attentively at the camera, with wire railings and blurred greenery in the background.</figcaption>
</figure>
<p>Without MCP, I can ask a model to look at the image and tell me what it sees:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ch</span> <span class="o">&lt;-</span> <span class="nf">chat_claude</span><span class="p">(</span><span class="s">&#34;Be brief.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ch</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">tools[[1]]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ch</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;What do you see in the reference image?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; I can see a beautiful Border Collie dog in the reference</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; image. The dog has:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - A chocolate brown and white coat with distinctive</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   coloring</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - Amber/brown eyes</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - Alert, perked ears</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - A white blaze down the center of its face</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - A pink/brown nose</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - Long, fluffy fur typical of the breed</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; The dog appears to be outdoors, positioned near what</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; looks like a wooden post or railing with wire fencing</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; visible in the background. There&#39;s greenery and trees</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; visible in the blurred background. The dog has an</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; attentive, intelligent expression that&#39;s characteristic</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; of Border Collies.</span></span></span></code></pre></div></div>
<p>In the next two sections, I&rsquo;ll deploy this function on Posit Connect and then fetch it from a fresh R session so that future ellmer chats can fetch this same image.</p>
<h2 id="mcp-servers-on-posit-connect">MCP Servers on Posit Connect
</h2>
<p>mcptools has now adopted plumber2&rsquo;s <code>_server.yml</code> open standard. This means that, <strong>to deploy an MCP server on Posit Connect, you just need to add a <code>_server.yml</code> file in your project root</strong> that looks like this:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">mcptools</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tools</span><span class="p">:</span><span class="w"> </span><span class="l">tools.R</span></span></span></code></pre></div></div>
<p><code>tools.R</code> (or whatever you choose to name the file) is a file that defines ellmer tools and returns them in a list. In my case, <code>tools.R</code> looks exactly like the code chunk defining <code>tools</code> above. I can then run <code>rsconnect::deployAPI(&quot;.&quot;, contentCategory = &quot;mcp&quot;)</code> to deploy the tools from <code>tools.R</code> as an authenticated MCP server on Posit Connect.</p>
<h2 id="fetch-tools-as-r-functions-from-authenticated-mcp-servers">Fetch tools as R functions from authenticated MCP servers
</h2>
<p>Perhaps the biggest gap in mcptools before this release was that <code>mcp_tools()</code> did not support remote, authenticated MCP servers. Up to this point, I had written in documentation that folks ought to use <code>npx mcp-remote</code>, which converts remote MCP servers (like Slack, Confluence, or really any of the most well-adopted third-party MCP servers) into local ones. That meant that, even though mcptools only implemented the local half of the protocol, mcptools users could connect to remotely hosted MCP servers.</p>
<p>This is undesirable for a few reasons. For one, mcptools should &ldquo;just work&rdquo; without users having to install software from sources other than CRAN. Further, installing code via <code>npx</code> is particularly problematic; the node package registry has been the source of <a href="https://www.axios.com/2026/03/31/north-korean-hackers-implicated-in-major-supply-chain-attack" target="_blank" rel="noopener">a</a> <a href="https://arstechnica.com/security/2026/06/dozens-of-red-hat-packages-backdoored-through-its-offical-npm-channel/" target="_blank" rel="noopener">number</a> <a href="https://www.theregister.com/cyber-crime/2026/05/18/shai-hulud-copycat-hits-another-npm-package/5242180" target="_blank" rel="noopener">of</a> <a href="https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem" target="_blank" rel="noopener">particularly</a> <a href="https://unit42.paloaltonetworks.com/monitoring-npm-supply-chain-attacks/" target="_blank" rel="noopener">concerning</a> supply chain attacks recently.</p>
<p><strong><code>mcp_tools()</code> now natively supports fetching tools from authenticated, third-party MCP servers.</strong> For example, a configuration that used to look like:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;mcpServers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;connect&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;npx&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;mcp-remote&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&lt;my_deployed_connect_listing&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--header&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Authorization: Key ${CONNECT_API_KEY}&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Now looks like:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;mcpServers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;connect&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;my_deployed_connect_listing&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;headers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Key ${CONNECT_API_KEY}&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>I&rsquo;ll save that latter configuration as <code>config.json</code>. Then, I&rsquo;ll provide it to <code>mcp_tools()</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tools_fetched</span> <span class="o">&lt;-</span> <span class="nf">mcp_tools</span><span class="p">(</span><span class="n">config</span> <span class="o">=</span> <span class="s">&#34;config.json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">class</span><span class="p">(</span><span class="n">tools_fetched[[1]]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;ellmer::ToolDef&#34; &#34;function&#34;        &#34;S7_object&#34;</span></span></span></code></pre></div></div>
<p>Now I can register the fetched tools with a chat in a new R session, and it has access to the same image:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ch_new</span> <span class="o">&lt;-</span> <span class="nf">chat_claude</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">ch_new</span><span class="o">$</span><span class="nf">register_tools</span><span class="p">(</span><span class="n">tools_fetched</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ch_new</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;What&#39;s in the reference image?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; The reference image features a **Border Collie** dog.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Here are some details:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Coloring**: The dog has a striking **brown</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   (chocolate) and white** coat, with a distinctive white</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   stripe running down the center of its face.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Expression**: It has an alert and attentive look,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   with beautiful **amber/brown eyes**.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Tongue**: Its tongue is slightly visible, giving it a</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   cute appearance.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Setting**: The dog appears to be on a **deck or</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   porch**, with cable/wire railings visible, and a lush</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   green, leafy background suggesting an outdoor, wooded</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   area.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Accessories**: It appears to be wearing a small</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   **collar tag**.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Overall, it&#39;s a beautiful and expressive dog photo! 🐕</span></span></span></code></pre></div></div>
<p>Taken together, the changes in this release should allow R users to do much more with MCP! For a more complete list of changes in this release, see the package <a href="https://posit-dev.github.io/mcptools/news/index.html" target="_blank" rel="noopener">changelog</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-06_mcptools-1-0-0/featured.png" length="874368" type="image/png" />
    </item>
    <item>
      <title>AI Newsletter: AGENTS.md vs Skills vs MCP servers</title>
      <link>https://opensource.posit.co/blog/2026-07-03_ai-newsletter/</link>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-03_ai-newsletter/</guid>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<h2 id="prompt-agentsmd-skill-or-mcp-server">Prompt, AGENTS.md, skill, or MCP server?
</h2>
<p>There are a variety of ways to give a coding agent new information or abilities, but it can be confusing when to use each one. Generally, with coding agents like Claude Code or Posit Assistant, you can:</p>
<ul>
<li><strong>Write prompts</strong> in the chat (i.e., normal usage).</li>
<li>Add information to either a project-level or user-level <strong><code>CLAUDE.md</code> or <code>AGENTS.md</code>.</strong> The contents of a directory&rsquo;s <code>CLAUDE.md</code> or <code>AGENTS.md</code> are included in the agent&rsquo;s system prompt in every session in that directory. You can also create user-level versions that apply to every session.</li>
<li>Write a <strong>skill</strong> or use an existing one. Skills are packaged instructions that can include both text and code. The agent loads a skill only when it&rsquo;s relevant.</li>
<li>Add an <strong>MCP server.</strong> An <a href="https://modelcontextprotocol.io/docs/getting-started/intro" target="_blank" rel="noopener">MCP (Model Context Protocol)</a> server provides an agent with access to otherwise hard-to-find context, mostly through <a href="https://modelcontextprotocol.io/docs/learn/server-concepts#tools" target="_blank" rel="noopener">tools</a>, using a standardized interface.</li>
</ul>
<p>This list is roughly ordered from most straightforward to most complicated.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>So when do you use one over the other? There are two axes that might matter for your decision.</p>
<p>The first axis is reusability: do you want the agent to perform the task or access the information just once, or many times? The more often you or others will reuse something, the more it&rsquo;s worth encoding somewhere more permanent.</p>
<p>The second axis is reach. Prompting, <code>CLAUDE.md</code>/<code>AGENTS.md</code> files, and skills all provide guidance on how to best make use of the existing context and tools. MCP servers can provide the agent with entirely new tools (in the <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">agent tools</a> sense), granting the agent access to hard-to-reach information.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-07-03_ai-newsletter/images/diagram.excalidraw.svg"
      alt="Decision tree for choosing how to customize a coding agent. The first question asks whether this is a one-time thing or something you&rsquo;ll need the agent to do similarly in the future. A one-time thing leads to normal prompting. If you&rsquo;ll need it again, the next question is when you want the agent to do the task: every session in the project leads to a CLAUDE.md or AGENTS.md file, while &ldquo;as needed&rdquo; leads to a further question. That question asks whether the agent has the capability to do the task with the tools it already has: if yes, write a skill; if no, use an MCP server."  title="A decision tree for choosing between prompting, AGENTS.md, skills, and MCP servers." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">A decision tree for choosing between prompting, AGENTS.md, skills, and MCP servers.</figcaption>
  </figure></div>
</p>
<p><strong>MCP servers often seem like the solution when you need to grant an agent access to an outside system, but they aren&rsquo;t always necessary.</strong> In many cases, what seems like a task for an MCP server can actually be solved by a command-line interface (CLI) tool, or a CLI plus a skill that tells the agent how to use it. For example, GitHub has an <a href="https://github.com/github/github-mcp-server" target="_blank" rel="noopener">MCP server</a>, but the <a href="https://cli.github.com/" target="_blank" rel="noopener"><code>gh</code></a> CLI does roughly the same thing.</p>
<p>The GitHub MCP server works by providing the agent with new tools, whereas the <code>gh</code> CLI takes advantage of the agent&rsquo;s existing bash tool that lets it run arbitrary shell commands. The skill plus CLI option is therefore generally preferable from a simplicity standpoint, but also from a token standpoint: adding the GitHub MCP server would inject tens of thousands of tokens of tool definitions into every request, whereas a skill that tells the agent to use <code>gh</code> costs almost nothing and loads only when it&rsquo;s relevant.</p>
<p>However, some information sources are hard to reach via the command line, because no CLI exists, the one that does isn&rsquo;t fully featured, or it&rsquo;s difficult for the agent to use. In some of these cases, the same sources can be accessed more effectively via MCP servers, such as design tools like Figma, knowledge repositories like Notion or Confluence, or issue trackers like Linear or Jira.</p>
<h2 id="posit-news">Posit news
</h2>
<h3 id="posit-assistant-in-positron">Posit Assistant in Positron
</h3>
<p>As of the <a href="https://positron.posit.co/download.html#release-notes" target="_blank" rel="noopener">June release</a> of Positron, Posit Assistant is now the default experience in Positron. Positron Assistant will be deprecated starting in the 2026.07 release (release date July 6).</p>
<p>We understand that the names are confusing! Our hope is that the transition state will be over soon and the confusion will lessen. If you want to understand why we gave different assistants very similar names, read this blog post from Posit CTO Joe Cheng: <a href="https://opensource.posit.co/blog/2026-06-11_history-of-posit-data-science-agents/" target="_blank" rel="noopener">A brief and biased history of Posit data science agents</a>.</p>
<p>Posit Assistant works with the same providers as Positron Assistant. If you have a working provider setup with Positron Assistant, you&rsquo;ll be able to use that same setup with Posit Assistant. You can read more about available providers <a href="https://assistant.posit.co/docs/downloads/positron/" target="_blank" rel="noopener">here</a>.</p>
<h3 id="package-updates">Package updates
</h3>
<ul>
<li><a href="https://opensource.posit.co/blog/2026-07-01_raghilda-0-2-0/" target="_blank" rel="noopener">raghilda v0.2</a>, a Python package for Retrieval Augmented Generation, is now on PyPI. This release, among other things, broadens support for crawling sites.</li>
<li><a href="https://opensource.posit.co/blog/2026-06-22_debrief-0-1-0/" target="_blank" rel="noopener">debrief</a>, an R package for LLM-friendly profiling, is now on CRAN. debrief turns profvis profiling output into text-based summaries, allowing AI agents to optimize R code more effectively.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>One interface we didn&rsquo;t mention here is &lsquo;custom agents,&rsquo; a concept popularized by GitHub Copilot and now appearing under various names. These bundle some combination of prompts and tools (sometimes gathered via MCP). We&rsquo;d reach for the options mentioned above first, which are open standards that are broadly supported across most agent platforms and thus can be shared and migrated more easily.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-03_ai-newsletter/images/hero.png" length="83964" type="image/png" />
    </item>
    <item>
      <title>posit::glimpse() Newsletter – July 2026</title>
      <link>https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/</link>
      <pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<blockquote>
<p>Welcome to our newsletter, posit::glimpse()!</p>
<p>If you&rsquo;re currently reading this on our blog, consider subscribing to Product Updates - Open Source on our <a href="https://posit.co/about/subscription-management" target="_blank" rel="noopener">subscription page</a> to receive this newsletter directly in your inbox.</p>
</blockquote>
<p>Welcome to the latest edition of the posit::glimpse() newsletter, the monthly roundup of open-source news for the Posit community. I have many updates for you from across Posit. As my wonderful colleague Kristin Bott stated, “<em>dang</em> this is a productive bunch of humans”.</p>
<p>The table of contents on the right can help you navigate through all the updates. As you scroll, it will open up to show you subcategories →</p>
<h2 id="announcements">Announcements
</h2>
<h3 id="positconf2026-is-happening-soon">posit::conf(2026) is happening soon!
</h3>
<p>Our annual conference, <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">posit::conf(2026)</a>, is happening September 14-16, and we would love to see you there, whether in Houston or online! Check out the <a href="https://posit.co/blog/posit-conf-2026-agenda-breakdown" target="_blank" rel="noopener">speaker lineup</a>, <a href="https://posit.co/blog/workshops-at-positconf2026" target="_blank" rel="noopener">workshop offerings</a>, and <a href="https://conf.posit.co/2026/registration/" target="_blank" rel="noopener">register for posit::conf here</a>.</p>
<p>Tidy Dev Day is happening on September 17, a unique opportunity to collaboratively tackle open-source issues and work directly alongside the very developers who build and maintain the tools you use every day. As <a href="https://thetidytrekker.com/post/vibe-conf-ing/posit_conf_2025" target="_blank" rel="noopener">Meghan Harris stated about last year’s event</a>, “Tidy Dev Day (TDD) gave me the PERFECT opportunity to explore this further in a low-stress, supportive environment.” <a href="https://opensource.posit.co/blog/2026-06-25_tidy-dev-day-2026/" target="_blank" rel="noopener">Learn more about Tidy Dev Day here</a>.</p>
<h3 id="weve-joined-the-jupyter-foundation">We’ve joined the Jupyter Foundation
</h3>
<p>We are proud to announce that we are deepening our commitment to the <a href="https://jupyter.org/" target="_blank" rel="noopener">Jupyter</a> ecosystem by becoming an official <a href="https://jupyterfoundation.org/" target="_blank" rel="noopener">Jupyter Foundation</a> Member!</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-25_posit-joins-jupyter-foundation/" target="_blank" rel="noopener">We’ve Joined the Jupyter Foundation announcement</a> blog post.</li>
</ul>
<h3 id="announcing-an-expanded-posit-academy">Announcing an expanded Posit Academy
</h3>
<p>We&rsquo;ve launched a new part of <a href="http://academy.posit.co/" target="_blank" rel="noopener">Posit Academy</a>: a free, open library of product courses, hands-on labs, and live workshops available to anyone.</p>
<ul>
<li>Learn more in the <a href="https://posit.co/blog/announcing-expanded-posit-academy" target="_blank" rel="noopener">Announcing an expanded Posit Academy</a> blog post.</li>
</ul>
<h3 id="introducing-the-posit-impact-awards">Introducing the Posit Impact Awards
</h3>
<p>Have a story to share? We just launched the Posit Impact Awards to recognize individuals and teams who used Posit to create measurable, meaningful change. Six winners will be selected, one per category, and each will receive a conference-only pass to posit::conf(2026).</p>
<ul>
<li><a href="https://docs.google.com/forms/d/e/1FAIpQLSfQrWnEQ_wlc5lhyn5BLgU0mvfWDXb1XSXhq9PoSERdWSZS3g/viewform" target="_blank" rel="noopener">Submit your nomination before July 20th.</a></li>
</ul>
<h2 id="key-product-updates-and-new-releases">Key product updates and new releases
</h2>
<h3 id="data-visualization-and-reporting">Data visualization and reporting
</h3>
<h4 id="ggsql-041">ggsql 0.4.1
</h4>
<p><a href="https://ggsql.org/" target="_blank" rel="noopener">ggsql</a> 0.4.1 introduces spatial plotting capabilities with database-backed geometry processing, supporting WKB format data, 21 map projections for cartographic accuracy, and a built-in Natural Earth world dataset for creating choropleth maps and geographic visualizations with backends like DuckDB spatial, PostGIS, and SpatiaLite.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-23_ggsql_0_4_1/" target="_blank" rel="noopener">ggsql 0.4.1: Spatial plotting and in-layer aggregation</a> blog post.</li>
</ul>
<h4 id="ask-more-of-your-dashboard-with-querychat-and-ggsql">Ask more of your dashboard with querychat and ggsql
</h4>
<p><a href="https://posit-dev.github.io/querychat/" target="_blank" rel="noopener">querychat</a> now supports ggsql-powered visualizations, enabling natural language data exploration in dashboards through SQL-only execution (no arbitrary code), with three pre-built tools for visualizing, querying, and filtering data reactively. The package works in both Python and R, integrates with <a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a> dashboards, and supports Snowflake Semantic Models for business logic definitions.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-17_querychat-ggsql/" target="_blank" rel="noopener">Ask more of your dashboard with querychat and ggsql</a> blog post.</li>
</ul>
<h4 id="great-tables-0220">Great Tables 0.22.0
</h4>
<p><a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables</a> v0.22.0 significantly expands Python table presentation capabilities with footnote support, group-wise and grand summary row calculations, column merging utilities for uncertainty and ranges, text transformation methods, value substitution helpers, duration and parts-per formatters, image export functionality via gtsave(), enhanced LaTeX rendering, and makes Pandas an optional dependency for Polars-only workflows. (impressive update!)</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/" target="_blank" rel="noopener">Great Tables v0.22.0</a> blog post.</li>
</ul>
<h3 id="data-access">Data access
</h3>
<h4 id="dbplyr-260">dbplyr 2.6.0
</h4>
<p><a href="https://dbplyr.tidyverse.org/" target="_blank" rel="noopener">dbplyr</a> 2.6.0 introduces ADBC support via adbi for faster Arrow-based data transfer, JDBC support, new SQL dialect separation, and query composition functions.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-17_dbplyr-2-6-0/" target="_blank" rel="noopener">dbplyr 2.6.0</a> blog post.</li>
</ul>
<h4 id="webr-060">webR 0.6.0
</h4>
<p><a href="https://docs.r-wasm.org/webr/latest/" target="_blank" rel="noopener">webR</a> 0.6.0 upgrades to R 4.6.0 and adds async/await support for JavaScript Promises, curl and httr2 compatibility through WebSocket traffic proxying, modern Fortran fixes for expanded package support, and updated system libraries including OpenSSL 3.5.1 and Emscripten 5.0.7. The release powers interactive R experiences in Quarto Live and Shinylive.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-18_webr-0-6-0/" target="_blank" rel="noopener">webR 0.6.0</a> blog post.</li>
</ul>
<h3 id="developer-tools-and-ai">Developer tools and AI
</h3>
<h4 id="debrief-010">debrief 0.1.0
</h4>
<p>The <a href="https://r-lib.github.io/debrief/" target="_blank" rel="noopener">debrief</a> package converts profvis profiling output into text-based summaries designed for AI agents, enabling AI-assisted performance optimization by providing structured reports on hotspots, call trees, and memory allocations that AI systems can read and act upon.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-22_debrief-0-1-0/" target="_blank" rel="noopener">debrief 0.1.0</a> blog post.</li>
</ul>
<h4 id="pkgsite-010">pkgsite 0.1.0
</h4>
<p><a href="https://edgararuiz.github.io/pkgsite/" target="_blank" rel="noopener">pkgsite</a> 0.1.0 converts R package .Rd documentation files into <a href="https://quarto.org/" target="_blank" rel="noopener">Quarto</a> .qmd files, enabling custom documentation sites with Quarto’s freeze feature for local example rendering, unified R/Python documentation when combined with Quartodoc, and flexible template customization. The package is available on CRAN and provides an alternative to pkgdown.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-18_pkgsite-0-1-0/" target="_blank" rel="noopener">pkgsite 0.1.0</a> blog post.</li>
</ul>
<h4 id="watcher-020">watcher 0.2.0
</h4>
<p><a href="https://watcher.r-lib.org/" target="_blank" rel="noopener">Watcher</a> is a lightweight R package that watches files and directories for changes and reacts in the background. It’s quietly been the engine behind Shiny&rsquo;s auto-reload for the past year. With the CRAN release of 0.2.0, we&rsquo;re excited to introduce it as a general-purpose filesystem watcher for R developers.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/" target="_blank" rel="noopener">watcher 0.2.0</a> blog post.</li>
</ul>
<h3 id="development-environment">Development environment
</h3>
<h4 id="air-0100">Air 0.10.0
</h4>
<p><a href="https://posit-dev.github.io/air/" target="_blank" rel="noopener">Air</a> 0.10.0 introduces configurable assignment style enforcement allowing teams to standardize on arrow (<code>&lt;-</code>), equal (<code>=</code>), or preserve existing styles, along with enhanced IDE integrations for Positron and RStudio, multiple installation methods via PyPI and conda-forge, pre-commit hook support, stdin integration for editors, and shell completions.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/" target="_blank" rel="noopener">Air 0.10.0</a> blog post.</li>
</ul>
<h4 id="whats-new-in-positron">What’s new in Positron
</h4>
<p><a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a>’s June release includes a lot of highly requested features:</p>
<ul>
<li>Inline output for Quarto (one of Positron’s most-requested features ever!)</li>
<li>Posit Assistant, the successor to Positron Assistant</li>
<li>Packages pane improvements</li>
<li>A more customizable interface</li>
</ul>
<p>For more, check out the <a href="https://opensource.posit.co/blog/2026-06-08_positron-2026-06-release/" target="_blank" rel="noopener">Positron June Release Highlights</a> post and <a href="https://posit.co/positron-updates-signup" target="_blank" rel="noopener">subscribe to Positron emails</a>.</p>
<p>Did you know that many of the most upvoted RStudio feature requests are already implemented in Positron? Learn about ten of them in the <a href="https://opensource.posit.co/blog/2026-06-10_rstudios-top-features-in-positron/" target="_blank" rel="noopener">RStudio’s Top Feature Requests … In Positron blog post</a>!</p>
<h3 id="machine-learning-and-modeling">Machine learning and modeling
</h3>
<h4 id="brulee-100">brulee 1.0.0
</h4>
<p><a href="https://brulee.tidymodels.org/" target="_blank" rel="noopener">brulee</a> 1.0.0 significantly expands tabular deep learning capabilities in R with five new model architectures, GPU support including Apple Silicon, 32-bit precision for improved performance, and enhanced numerical stability, all integrated with the tidymodels ecosystem.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-24_brulee-1-0-0/" target="_blank" rel="noopener">brulee 1.0.0</a> blog post.</li>
</ul>
<h4 id="catboost-support-in-tidymodels">CatBoost support in tidymodels
</h4>
<p>CatBoost gradient boosting support is now available in <a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels</a> through the <code>boost_tree()</code> interface, providing access to CatBoost’s strong categorical feature handling with full tidymodels integration including hyperparameter tuning, cross-validation, efficient submodel optimization, and orbital package support for SQL generation and in-database predictions.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-25_catboost-tidymodels/" target="_blank" rel="noopener">CatBoost support in tidymodels</a> blog post.</li>
</ul>
<h4 id="tidyclust-030">tidyclust 0.3.0
</h4>
<p><a href="https://tidyclust.tidymodels.org/index.html" target="_blank" rel="noopener">tidyclust</a> 0.3.0 introduces three new clustering model families and achieves full integration with tidymodels by replacing tidyclust-specific functions with native tune package support.</p>
<ul>
<li>Learn more in the <a href="https://opensource.posit.co/blog/2026-06-15_tidyclust-0-3-0/" target="_blank" rel="noopener">tidyclust 0.3.0</a> blog post.</li>
</ul>
<h2 id="event-roundup">Event roundup
</h2>
<p>We were all over the world this month, discussing how to adopt new data tools, get better at old ones, and just loving being part of the community. If you want to learn more about where we were, or see where we’ll be next, check out our <a href="https://opensource.posit.co/events/" target="_blank" rel="noopener">event page</a>.</p>
<p>Watch the recordings from some of these events:</p>















  
  
  
  
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  



<div class="grid grid-cols-2 gap-6 xl:gap-8 xl:gap-8 mb-6">
  
    <div>
      











  
  
  
  
  
















  
  
    
  











  
  





  
    
  









  
  






  





  



  

  

  

  

  

  

  

  

  

  

  

  

  

  




















  
    
  





  
    
  
























<div id="item-27ed749654bd2d6c044eb1d6fb3bf2e9" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/" title="Neal Richardson - MCP, or not MCP | Pydata London 26"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    <div class="w-full @venti:w-1/2 aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_2e4f2a3a58db4627.webp 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_3bea74d726538bdd.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_7524b87c91de63ad.jpg"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_7524b87c91de63ad.jpg 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_e7f65162a2cb51e6.jpg 2x"
          alt="Neal Richardson - MCP, or not MCP | Pydata London 26"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_3bea74d726538bdd.webp 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_168415f79709c6d1.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_e7f65162a2cb51e6.jpg"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_e7f65162a2cb51e6.jpg 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_409671ed69280e4b.jpg 2x"
          alt="Neal Richardson - MCP, or not MCP | Pydata London 26"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_168415f79709c6d1.webp 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_3829c4c9d7cfb126.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_409671ed69280e4b.jpg"
          srcset="/resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_409671ed69280e4b.jpg 1x,
                  /resources/videos/2026-06-15_neal-richardson-mcp-or-not-mcp-pydata-london-26/thumbnail_hu_99b33ee74fa66f52.jpg 2x"
          alt="Neal Richardson - MCP, or not MCP | Pydata London 26"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jun 15, 2026</span>
          <span>|</span>
        
        
        
          <span>34 min</span>
          <span>|</span>
        
        
          <span>308 views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-3xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-3xl text-gray-600">Neal Richardson - MCP, or not MCP | Pydata London 26</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">Neal Richardson - MCP, or not MCP | Pydata London 26 \Model Context Protocol is a standard for defining tools that can be made available to LLMs and AI applications. There’s a lot of noise out there about what you should use to get the best results from AI, so in this talk, I will provide some guidance on when you should use MCP, and when you should reach for some other tool. I will describe cases where MCP is the right tool for the job, and when other things, like skills or other context files, are better. I will also devote attention to questions of security and authentication, which are important for MCP, and provide concrete examples of how MCP servers can be used to unlock agentic workflows while also strengthening data governance. This talk is intended for those who are interested in using LLMs for workflows involving data. No prior experience with MCP is required.
Outline:
Intro: how can I get data from this API into my Claude Code session? What is MCP? When should you use it, when should you use other tools Work through an example Sharing and deploying MCP servers, alternatives and best practices Optimizing your tools for best results www.pydata.org
PyData is an educational program of NumFOCUS, a 501(c)3 non-profit organization in the United States. PyData provides a forum for the international community of users and developers of data analysis tools to share ideas and learn from each other. The global PyData network promotes discussion of best practices, new approaches, and emerging technologies for data management, processing, analytics, and visualization. PyData communities approach data science using many languages, including (but not limited to) Python, Julia, and R.
PyData conferences aim to be accessible and community-driven, with novice to advanced level presentations. PyData tutorials and talks bring attendees the latest project features along with cutting-edge use cases.
00:00 Welcome! 00:10 Help us add time stamps or captions to this video! See the description for details.
Want to help add timestamps to our YouTube videos to help with discoverability? Find out more here: https://github.com/numfocus/YouTubeVideoTimestamps
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/neal-richardson/profile.jpg" 
          alt="Neal Richardson" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Neal Richardson</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                Python
              </span>
            
              <span class="pill">
                Tutorial
              </span>
            
              <span class="pill">
                Education
              </span>
            
              <span class="pill">
                NumFOCUS
              </span>
            
              <span class="pill">
                PyData
              </span>
            
              <span class="pill">
                Opensource
              </span>
            
              <span class="pill">
                Learn
              </span>
            
              <span class="pill">
                Software
              </span>
            
              <span class="pill">
                Python 3
              </span>
            
              <span class="pill">
                Julia
              </span>
            
              <span class="pill">
                Coding
              </span>
            
              <span class="pill">
                Learn to Code
              </span>
            
              <span class="pill">
                How to Program
              </span>
            
              <span class="pill">
                Scientific Programming
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  











  
  





  
    
  









  
  






  





  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-0bea2d9d122698cc4c259ae2a49344c9" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/" title="Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    <div class="w-full @venti:w-1/2 aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_b03c3320e4321048.webp 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_709131f576b34e9b.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_946b93830a7ce124.jpg"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_946b93830a7ce124.jpg 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_6f49f3aa2116d34a.jpg 2x"
          alt="Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_709131f576b34e9b.webp 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_7b164a87556be464.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_6f49f3aa2116d34a.jpg"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_6f49f3aa2116d34a.jpg 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_2dcdb2aac4711fb1.jpg 2x"
          alt="Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_7b164a87556be464.webp 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_a0b0c1c85b31ff70.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_2dcdb2aac4711fb1.jpg"
          srcset="/resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_2dcdb2aac4711fb1.jpg 1x,
                  /resources/videos/2026-06-26_agents-for-correct-transparent-and-reproducible-data-analysis-simon-couch-sara-altman/thumbnail_hu_f7c8a43de74d743a.jpg 2x"
          alt="Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jun 26, 2026</span>
          <span>|</span>
        
        
        
          <span>47 min</span>
          <span>|</span>
        
        
          <span>179 views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-3xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-3xl text-gray-600">Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">Agents for Correct, Transparent, and Reproducible Data Analysis - Simon Couch &amp; Sara Altman (Posit)
Abstract: How do we build competent data analysis agents? Data analysis requires a willingness to pause, question conclusions, and dig into subtleties. Frontier LLMs, however, are optimized to push tasks toward completion, not to slow down when something seems off. This tendency works well for coding agents, where success is often verifiable. But for data analysis, verification is more complicated, and autonomous work by the agent can be at odds with the spirit of the discipline. Drawing on our experience building data analysis agents, we&rsquo;ll share evaluations that expose where LLM-driven analysis goes wrong and design patterns that keep analyses correct, transparent, and reproducible.
Resources mentioned in the session:
Presentation Slides: https://simonpcouch.github.io/gen-ai-pharma-26 Presentation GitHub repository: https://github.com/simonpcouch/gen-ai-pharma-26 bluffbench: https://github.com/simonpcouch/bluffbench Posit Assistant Terminal (TUI): https://posit-dev.github.io/assistant/docs/downloads/tui/ Posit AI Newsletter: https://opensource.posit.co/tags/ai-newsletter/ Speakers:
Simon Couch builds tools that make the work of data science more joyful and effective. As an engineer on the AI Core Team at Posit, his work spans coding agents, model evaluations, inference engineering, and next-edit-suggestion systems. Drawing on his background in statistics, Simon spent several years authoring and maintaining core packages in the open-source tidymodels framework—like stacks, broom, and infer — before shifting his focus to LLMs. He blogs about his work at simonpcouch.com. Simon authors the Posit AI Newsletter along with Sara Altman.
Sara Altman is a Senior Developer Advocate on the AI Core team at Posit, where she focuses on how AI can be effectively and responsibly used for data science. Previously, she helped build Posit Academy and taught data science and R at Stanford. Sara authors the Posit AI Newsletter along with Simon Couch.
Presented at the 2026 R/Pharma GenAI Day
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/sara-altman/profile.jpg" 
          alt="Sara Altman" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ><img 
          src="https://opensource.posit.co/people/simon-couch/profile.jpg" 
          alt="Simon Couch" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white -ml-2"
          style="z-index: 9;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Sara Altman,&nbsp;Simon Couch</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                infer
              </span>
            
              <span class="pill">
                tidymodels
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
</div>


<h2 id="showcases-from-the-community">Showcases from the community
</h2>









  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><p>Brilliant Earth turned their Marketing Mix Model into a <a href="https://streamlit.io/" target="_blank" rel="noopener">Streamlit</a> app deployed on <a href="https://posit.co/products/enterprise/connect" target="_blank" rel="noopener">Posit Connect</a> via the <a href="https://docs.posit.co/partnerships/snowflake/" target="_blank" rel="noopener">Snowflake Native App</a>, so their marketing team can dig into channel performance and run scenario planning on their own. One of the campaigns in the mix: their recent Ring Pop collaboration.</p>
<ul>
<li><a href="https://posit.co/about/customer-stories/brilliant-earth" target="_blank" rel="noopener">Check out the spotlight.</a></li>
</ul></div>
    
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/images/image01.png"
        alt="Brilliant Earth logo with rings in the background." 
        loading="lazy"
      >
    </figure></div></div>
    
  
</div>










  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/images/image02.png"
        alt="Dashboard showing Lorcana market data analysis with multiple charts displaying price trends and forecasting visualizations" 
        loading="lazy"
      >
    </figure></div></div>
    
  
    
    
    
      <div class="prose max-w-none "><p><a href="https://www.linkedin.com/in/leo-ohyama-phd-52358387/" target="_blank" rel="noopener">Leo Ohyama</a> recently shared a fantastic Lorcana Market Data Analysis &amp; Forecasting dashboard that showcases the power of Python, R, Positron, and Quarto working together.</p>
<p>Leo included detailed documentation in the GitHub repository, a great resource for anyone interested in market forecasting or multi-tool workflows. Thanks for sharing your work with the community, Leo!</p>
<ul>
<li><a href="https://lorecaster.ink/" target="_blank" rel="noopener">Lorcana dashboard</a></li>
<li><a href="https://github.com/leoohyama/lorcana" target="_blank" rel="noopener">Lorcana GitHub repo</a></li>
</ul>
</div>
    
  
</div>










  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><p>We were recently joined by <a href="https://opensource.posit.co/people/thomas-lin-pedersen/" target="_blank" rel="noopener">Thomas Lin Pedersen</a> on the <a href="https://pos.it/dslab" target="_blank" rel="noopener">Data Science Lab</a>, where he introduced the new <a href="https://ggsql.org/" target="_blank" rel="noopener">ggsql</a> package.</p>
<p>(Almost) immediately after the DS Lab, <a href="https://www.linkedin.com/in/drspoulsen/" target="_blank" rel="noopener">Dylan Poulsen</a> wrote a blog post on exploring two years of swim data with ggsql! Dylan, we’re convinced you write blogs at the speed of ggsql.</p>
<ul>
<li><a href="https://dylanpoulsen.com/posts/2026-06-16-ggsql-swimming.html" target="_blank" rel="noopener">Exploring the New ggsql Package with Two Years of Swim Data</a> blog post</li>
</ul>
</div>
    
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/images/image03.png"
        alt="Visualization created with ggsql showing swim data analysis with line charts tracking performance over time" 
        loading="lazy"
      >
    </figure></div></div>
    
  
</div>

<p>We usually find these projects on social media. If you’re on LinkedIn, be sure to follow and tag <a href="https://www.linkedin.com/showcase/posit-open-source/" target="_blank" rel="noopener">Posit Open Source</a> for us to share the amazing things you’re working on!</p>
<h2 id="whats-next">What’s next
</h2>
<p>We’re taking a short break in July before returning with more community hangouts!</p>
<ul>
<li>On July 21, Gina Reynolds will join the Data Science Lab (a fun, chill time with live code) to show we can extend ggplot2 by creating our own custom extensions. Register here: <a href="https://pos.it/dslab" target="_blank" rel="noopener">https://pos.it/dslab</a></li>
</ul>
<p>In the meantime, check out some past Data Science Lab episodes:</p>















  
  
  
  
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  



<div class="grid grid-cols-3 gap-6 xl:gap-8 xl:gap-8 mb-6">
  
    <div>
      











  
  
  
  
  
















  
  
    
  











  
  





  
    
  









  
  






  





  
    
  
    
  
    
  
    
  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-e1a7fb00aebe5239e2fb98a5bfb01eea" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/" title="Async &amp; Parallel R with {mirai} | Charlie Gao | Data Science Lab"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    <div class="w-full @venti:w-1/2 aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_c3d165a6da89ccf6.webp 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_e010d66820a12428.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_39e3051bf743594c.jpg"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_39e3051bf743594c.jpg 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_8760612808811c9a.jpg 2x"
          alt="Async &amp; Parallel R with {mirai} | Charlie Gao | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_e010d66820a12428.webp 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_1e8d89ec085c5bba.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_8760612808811c9a.jpg"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_8760612808811c9a.jpg 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_ed5d4972ccad3c7.jpg 2x"
          alt="Async &amp; Parallel R with {mirai} | Charlie Gao | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_1e8d89ec085c5bba.webp 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_897267d62d43465d.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_ed5d4972ccad3c7.jpg"
          srcset="/resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_ed5d4972ccad3c7.jpg 1x,
                  /resources/videos/2026-05-29_async-parallel-r-with-mirai-charlie-gao-data-science-lab/thumbnail_hu_6f4f6696051268a6.jpg 2x"
          alt="Async &amp; Parallel R with {mirai} | Charlie Gao | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>May 29, 2026</span>
          <span>|</span>
        
        
        
          <span>57 min</span>
          <span>|</span>
        
        
          <span>1.3k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-3xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-3xl text-gray-600">Async &amp; Parallel R with {mirai} | Charlie Gao | Data Science Lab</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">The Data Science Lab is a live weekly call. Register at pos.it/dslab! Discord invites go out each week on lives calls. We&rsquo;d love to have you!
The Lab is an open, messy space for learning and asking questions. Think of it like pair coding with a friend or two. Learn something new, and share what you know to help others grow.
On this call, Libby Heeren is joined by Charlie Gao, who walks through async and parallel programming using the mirai package for R. Charlie is the author of {mirai} and {mori}!
Charlie demonstrates how mirai lets you run R code in parallel across multiple cores or even distributed across remote machines. He covers the key differences between parallel and async programming, shows how to set up worker daemons (said like &ldquo;demons&rdquo;), scale resources dynamically, and connect to remote machines via SSH. Whether you&rsquo;re running long computations, training models, or building Shiny apps, mirai helps you make the most of your computing resources without blocking your main R session:)
Hosting crew from Posit: Libby Heeren, Isabella Velasquez
Charlie Gao&rsquo;s GitHub: https://github.com/shikokuchuo Charlie Gao&rsquo;s Bluesky: https://bsky.app/profile/shikokuchuo.net Charlie Gao&rsquo;s LinkedIn: https://www.linkedin.com/in/charliegao/ Charlie Gao&rsquo;s Mastodon: https://fosstodon.org/@shikokuchuo
Resources mentioned in the video and chat: mirai package website: https://mirai.r-lib.org/ mirai GitHub repository: https://github.com/r-lib/mirai AskDeepSeek chatbot for mirai documentation: https://mirai.r-lib.org/ (click &ldquo;ask deep wiki&rdquo; button) mirai - Promises (Shiny and Plumber): https://mirai.r-lib.org/articles/v02-promises.html mirai - Serialization: https://mirai.r-lib.org/articles/v03-serialization.html mirai - OpenTelemetry: https://mirai.r-lib.org/articles/v05-opentelemetry.html mirai stop_mirai function: https://mirai.r-lib.org/reference/stop_mirai.html mirai skill for Claude Code: https://github.com/r-lib/mirai/blob/main/.claude/skills/mirai/SKILL.md Plumber2 package: https://plumber2.posit.co/ Daemon (computing) on Wikipedia: https://en.wikipedia.org/wiki/Daemon_(computing)
► Subscribe to Our Channel Here: https://bit.ly/2TzgcOu Follow Us Here: Website: https://www.posit.co Hangout: https://pos.it/dsh The Lab: https://pos.it/dslab LinkedIn: https://www.linkedin.com/company/posit-software Bluesky: https://bsky.app/profile/posit.co
Thanks for hanging out with us!
Timestamps of Questions / Topics: 00:00 Introduction 03:33 &ldquo;What is async programming and how is it different from parallel?&rdquo; 06:57 AskDeepSeek chatbot for the Mirai package 09:40 Setting up the Positron IDE with activity bar on top 15:25 &ldquo;What was the motivation or need for developing Mirai?&rdquo; 22:10 Mirai map function for parallel processing 25:25 &ldquo;Do the contents of Mirai inherit definitions from the global environment?&rdquo; 29:00 &ldquo;What&rsquo;s the difference between Mirai versus promises and future?&rdquo; 31:50 Demonstrating sequential vs parallel processing 36:00 &ldquo;Can you use Mirai with HPC?&rdquo; 37:07 Dynamically scaling workers by adding and removing daemons 38:38 Setting up daemons with URLs for network connections 42:05 Launching workers over SSH to remote machines 43:09 SSH tunneling to connect workers without open ports 51:14 &ldquo;Does Mirai allow R to perform the same thing as NumPy?&rdquo; 51:54 &ldquo;Does Mirai ship with some kind of task viewing dashboard?&rdquo; 52:35 &ldquo;Can we use parallel::detectCores() to see how many daemons we can use?&rdquo; 53:58 &ldquo;Would parallel processing be more useful than async processing in typical data science work?&rdquo; 55:00 Mirai skill for Claude Code and AI agents
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/charlie-gao/profile.jpg" 
          alt="Charlie Gao" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Charlie Gao</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                mirai
              </span>
            
              <span class="pill">
                mori
              </span>
            
              <span class="pill">
                plumber
              </span>
            
              <span class="pill">
                plumber2
              </span>
            
              <span class="pill">
                Positron
              </span>
            
              <span class="pill">
                Shiny
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  











  
  





  
    
  









  
  






  





  
    
  
    
  
    
  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-451ed6564c115dcc7bdce93d95ee2802" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/" title="Data dictionaries, parquet, &amp; Claude | Hadley Wickham | Data Science Lab"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    <div class="w-full @venti:w-1/2 aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_a23a02bc126732e3.webp 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_36f7dc1a902e03a.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_7d73634aa10248d.jpg"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_7d73634aa10248d.jpg 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_53383455a06a605f.jpg 2x"
          alt="Data dictionaries, parquet, &amp; Claude | Hadley Wickham | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_36f7dc1a902e03a.webp 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_5a96a040aff01af5.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_53383455a06a605f.jpg"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_53383455a06a605f.jpg 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_d35a1a3e9dce2346.jpg 2x"
          alt="Data dictionaries, parquet, &amp; Claude | Hadley Wickham | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_5a96a040aff01af5.webp 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_264773719908e6d9.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_d35a1a3e9dce2346.jpg"
          srcset="/resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_d35a1a3e9dce2346.jpg 1x,
                  /resources/videos/2026-06-23_data-dictionaries-parquet-claude-hadley-wickham-data-science-lab/thumbnail_hu_d52ea0ae3df67b45.jpg 2x"
          alt="Data dictionaries, parquet, &amp; Claude | Hadley Wickham | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jun 23, 2026</span>
          <span>|</span>
        
        
        
          <span>58 min</span>
          <span>|</span>
        
        
          <span>4.9k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-3xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-3xl text-gray-600">Data dictionaries, parquet, &amp; Claude | Hadley Wickham | Data Science Lab</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">The Data Science Lab is a live weekly call. Register at pos.it/dslab! Discord invites go out each week on lives calls. We&rsquo;d love to have you!
The Lab is an open, messy space for learning and asking questions. Think of it like pair coding with a friend or two. Learn something new, and share what you know to help others grow.
On this call, Libby Heeren is joined by Hadley Wickham, who walks through using data dictionaries with Claude Code to clean and document datasets effectively.
Hadley demonstrates a workflow using three files: a data cleaning script, a data dictionary in YAML format, and the final cleaned data as a Parquet file. He shows how Claude Code and MCP REPL can help generate and maintain data dictionaries that document what you know about your data, making it easier for both humans and AI agents to work with your datasets. Using the NYC elevators dataset as an example, he walks through data cleaning tasks like normalizing whitespace, handling missing values, fixing date formats, and investigating geocoding issues - all while keeping the data dictionary, cleaning script, and Parquet file in sync through git.
Hosting crew from Posit: Libby Heeren, Isabella Velasquez
Hadley Wickham&rsquo;s GitHub: https://github.com/hadley Hadley Wickham&rsquo;s Bluesky: https://bsky.app/profile/hadley.nz Hadley Wickham&rsquo;s LinkedIn: https://www.linkedin.com/in/hadleywickham/
Resources mentioned in the video and chat: MCP REPL: https://github.com/posit-dev/mcp-repl Data Dictionary YAML Format Specification: https://github.com/hadley/data-dict.yaml Parquet files in R for Data Science: https://r4ds.hadley.nz/arrow.html#sec-parquet Elevators Dataset Used in Demo: https://github.com/EmilHvitfeldt/elevators Pointblank Package for Data Validation: https://posit-dev.github.io/pointblank/ Arrow R Book: https://arrowrbook.com/ Monaspace Font Family (with ligatures): https://monaspace.githubnext.com/ YAML Multiline Strings Reference: https://yaml-multiline.info/ UBC Course on Shiny with RAG and Parquet: https://ubc-mds.github.io/DSCI_532_vis-2_book/060-03-rag.html Tom Scott Video on Timezones: https://www.youtube.com/watch?v=-5wpm-gesOY Falsehoods Programmers Believe About Names: https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/ UTC Is Enough for Everyone Right: https://zachholman.com/talk/utc-is-enough-for-everyone-right Daniel Chen&rsquo;s Cherry Blossom Analysis: https://chendaniely.github.io/posts/2026/2026-03-30-yvr-cherry-blossoms-marathon/ Project Drawdown Climate Impact Explorer: https://drawdown.org/explorer Green Coding Bookdown Resource: https://bookdown.org/content/d1e53ac9-28ce-472f-bc2c-f499f18264a3/ IBM Green Coding Topics: https://www.ibm.com/think/topics/green-coding Secret Elevator in Central Park Article: https://undercovernyc.home.blog/2021/02/08/a-secret-elevator-hidden-in-central-park/ Artificial Cave Beneath Central Park: https://gizmodo.com/an-artificial-cave-200-beneath-central-park-with-micha-1446538828
► Subscribe to Our Channel Here: https://bit.ly/2TzgcOu Follow Us Here: Website: https://www.posit.co Hangout: https://pos.it/dsh The Lab: https://pos.it/dslab LinkedIn: https://www.linkedin.com/company/posit-software Bluesky: https://bsky.app/profile/posit.co
Thanks for hanging out with us!
Timestamps of Questions / Topics: 00:00 Introduction 03:38 &ldquo;Can you talk a little bit about what MCP is?&rdquo; 05:42 Introducing the elevators dataset 07:00 Creating initial data dictionary with Claude 09:03 &ldquo;Are there any cases where the CSV format is actually a better choice than Parquet?&rdquo; 09:50 &ldquo;What font do you use?&rdquo; 12:02 Adding context from the readme to the data dictionary 14:02 &ldquo;What is a good way to store data dictionaries along with datasets?&rdquo; 14:38 &ldquo;Is this particular data dict YAML format useful for projects with only one table of data?&rdquo; 17:45 &ldquo;Is Claude also going to decide when it&rsquo;s a good time to make a commit?&rdquo; 24:02 &ldquo;Does MCP REPL work on a Windows machine and can one use other AI LLM for example ChatGPT with it?&rdquo; 26:14 Converting date columns to proper date types 27:16 &ldquo;Do you see a meaningful distinction between data dicts and data contracts?&rdquo; 29:36 &ldquo;How do you view your approach to data dictionaries and the development of pointblank?&rdquo; 34:16 Eliminating placeholder values and using proper missing values 36:25 &ldquo;Could you show off a diff of a Parquet file?&rdquo; 38:25 Investigating geocodes and creating a map of elevators 40:20 Using a leaflet map to explore Central Park elevators 42:03 &ldquo;Which model is Hadley using?&rdquo; 43:55 Discussion of cost consciousness and environmental impact of LLMs 46:30 &ldquo;Is there a way to quantify environmental and electrical costs?&rdquo; 48:50 The mystery elevator in Central Park 54:23 &ldquo;How do you know this is actually faster or more productive than just writing the code?&rdquo; 55:45 The importance of deep knowledge of data in qualitative work
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/hadley-wickham/profile.jpg" 
          alt="Hadley Wickham" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Hadley Wickham</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                bookdown
              </span>
            
              <span class="pill">
                bookdown.org
              </span>
            
              <span class="pill">
                leaflet
              </span>
            
              <span class="pill">
                pointblank
              </span>
            
              <span class="pill">
                Shiny
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  











  
  





  
    
  









  
  






  





  
    
  
    
  
    
  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-4d9af617f6704fb38d6f46af20c8ee48" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/" title="Using R package structure for data science projects | Kylie Ainslie | Data Science Lab"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    <div class="w-full @venti:w-1/2 aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_b342729d310b0653.webp 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_2d213a05d6559bee.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_5eb9cd45b958caea.jpg"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_5eb9cd45b958caea.jpg 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_2d34cd327bb21020.jpg 2x"
          alt="Using R package structure for data science projects | Kylie Ainslie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_2d213a05d6559bee.webp 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_7968fe6f5db9f34d.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_2d34cd327bb21020.jpg"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_2d34cd327bb21020.jpg 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_7edb6814911cc29d.jpg 2x"
          alt="Using R package structure for data science projects | Kylie Ainslie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_7968fe6f5db9f34d.webp 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_74bb543c4243eb29.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_7edb6814911cc29d.jpg"
          srcset="/resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_7edb6814911cc29d.jpg 1x,
                  /resources/videos/2026-02-19_using-r-package-structure-for-data-science-projects-kylie-ainslie-data-science-lab/thumbnail_hu_b7f23d7c59f3ea7c.jpg 2x"
          alt="Using R package structure for data science projects | Kylie Ainslie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Feb 19, 2026</span>
          <span>|</span>
        
        
        
          <span>55 min</span>
          <span>|</span>
        
        
          <span>1.7k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-3xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-3xl text-gray-600">Using R package structure for data science projects | Kylie Ainslie | Data Science Lab</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">The Data Science Lab is a live weekly call. Register at pos.it/dslab! Discord invites go out each week on lives calls. We&rsquo;d love to have you!
The Lab is an open, messy space for learning and asking questions. Think of it like pair coding with a friend or two. Learn something new, and share what you know to help others grow.
On this call, Libby Heeren is joined by Kylie Ainslie who walks through how structuring data science projects as R packages provides a consistent framework that integrates documentation for you and facilitates collaboration with others by organizing things really well. Kylie says, &ldquo;I stumbled on using an R package structure to organize my projects a number of years ago and it has changed how I work in such a positive way that I want to share it with others! In a world where our attention is constantly being pulled in many directions, efficiency is crucial. Structuring projects as R packages is how I work more efficiently.&rdquo;
Hosting crew from Posit: Libby Heeren, Isabella Velasquez
Kylie&rsquo;s Bluesky: @kylieainslie.bsky.social Kylie&rsquo;s LinkedIn: https://www.linkedin.com/in/kylieainslie/ Kylie&rsquo;s Website: https://kylieainslie.github.io/ Kylie&rsquo;s GitHub: https://github.com/kylieainslie
Resources from the hosts and chat:
posit::conf(2026) call for talks: https://posit.co/blog/posit-conf-2026-call-for-talks/ Kylie&rsquo;s posit::conf(2025) talk: https://www.youtube.com/watch?v=YzIiWg4rySA {usethis} package: https://usethis.r-lib.org/ R Packages (2e) book: https://r-pkgs.org/ Paquetes de R (R Packages in Spanish): https://davidrsch.github.io/rpkgs-es/ {box} package: https://github.com/klmr/box extdata docs in Writing R Extensions: https://cran.r-project.org/doc/manuals/R-exts.html#Data-in-packages-1 Tan Ho&rsquo;s talk on NFL data: https://tanho.ca/talks/rsconf2022-github/ {rv} package: https://a2-ai.github.io/rv-docs/ Whether to Import or Depend: https://r-pkgs.org/dependencies-mindset-background.html#sec-dependencies-imports-vs-depends {pkgdown} package: https://pkgdown.r-lib.org/ Edgar Ruiz&rsquo;s {pkgsite} package: https://github.com/edgararuiz/pkgsite
Attendees shared examples of data packages in the chat! Here they are: https://kjhealy.github.io/nycdogs/ https://kjhealy.github.io/gssr/ https://github.com/deepshamenghani/richmondway https://github.com/kyleGrealis/nascaR.data https://github.com/ivelasq/leaidr
► Subscribe to Our Channel Here: https://bit.ly/2TzgcOu
Follow Us Here: Website: https://www.posit.co The Lab: https://pos.it/dslab Hangout: https://pos.it/dsh LinkedIn: https://www.linkedin.com/company/posit-software Bluesky: https://bsky.app/profile/posit.co
Thanks for learning with us!
Timestamps: 00:00 Introduction 06:17 Reviewing the disorganized project example 10:01 Creating the package structure using create_package 17:50 Organizing external data and scripts in the inst folder 22:55 Adding a README and License 29:06 &ldquo;What are the advantages to packaging a project?&rdquo; 33:35 Writing Roxygen2 documentation 36:06 &ldquo;Do you type return at the end of your functions?&rdquo; 41:55 Handling dependencies with use_package 43:53 &ldquo;Can you just use require(dplyr) at the top?&rdquo; 47:45 Setting up a pkgdown site 50:11 Creating vignettes 52:22 &ldquo;What is the role of the usethis package?&rdquo; 54:18 Loading the package with devtools::load_all
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/edgar-ruiz/profile.jpg" 
          alt="Edgar Ruiz" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Edgar Ruiz</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                devtools
              </span>
            
              <span class="pill">
                dplyr
              </span>
            
              <span class="pill">
                pkgdown
              </span>
            
              <span class="pill">
                roxygen2
              </span>
            
              <span class="pill">
                usethis
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
</div>


<p>I’m a real person, and I would love to know how to make the Glimpse newsletter better! Find me on <a href="https://www.linkedin.com/in/ivelasq/" target="_blank" rel="noopener">LinkedIn</a> and <a href="https://bsky.app/profile/ivelasq3.bsky.social" target="_blank" rel="noopener">Bluesky</a>, or email me at isabella [dot] velasquez [at] posit.co.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-01_2026-07-glimpse/thumbnail.jpg" length="68574" type="image/jpeg" />
    </item>
    <item>
      <title>raghilda `v0.2`: crawl APIs, PostgreSQL, and more</title>
      <link>https://opensource.posit.co/blog/2026-07-01_raghilda-0-2-0/</link>
      <pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-01_raghilda-0-2-0/</guid>
      <dc:creator>Rich Iannone</dc:creator>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p>When we <a href="https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/" target="_blank" rel="noopener">introduced
raghilda</a>
in April, the package handled the core RAG workflow: read documents,
chunk them, embed them into a store, and retrieve relevant chunks at
query time. raghilda <code>v0.2</code> broadens that scope considerably. The
release adds a structured crawl and ingest API that replaces the manual
read-and-upsert loop with a pipeline built around caching, concurrency,
and composable crawlers (including a <code>CloudflareCrawler</code> that can index
JavaScript-rendered sites without running a local headless browser). It
also ships a PostgreSQL store backend and NVIDIA NIM embedding support.</p>
<p>This post walks through the major additions. The package&rsquo;s fundamentals
have not changed (stores, chunkers, retrievers, and the pattern for
connecting to chatlas all work as before), but the surface area for
building and maintaining stores in production has grown substantially.</p>
<h2 id="the-crawl-and-ingest-api">The crawl and ingest API
</h2>
<p>The largest change in <code>v0.2</code> is a new API for crawling sources and
ingesting them into a store. In <code>v0.1</code>, building a store meant calling
<code>read_as_markdown()</code> on individual URLs, chunking each result, and
upserting them one by one. That works for a handful of pages, but it
becomes unwieldy for larger collections, where you also want caching (to
avoid re-fetching unchanged content) and concurrency (to finish in
minutes rather than hours).</p>
<p>The new API introduces a clean separation between crawling and storage.
On the crawl side, a crawler object produces <code>MarkdownDocument</code> objects
from a defined scope. On the store side, <code>store.ingest()</code> consumes those
documents lazily, applies an optional preparation step (typically
chunking), and writes them to the store with configurable parallelism.
The pipeline looks like this:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.chunker</span> <span class="kn">import</span> <span class="n">MarkdownChunker</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.crawl</span> <span class="kn">import</span> <span class="n">CrawlScope</span><span class="p">,</span> <span class="n">WebCrawler</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.embedding</span> <span class="kn">import</span> <span class="n">EmbeddingOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.store</span> <span class="kn">import</span> <span class="n">DuckDBStore</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">crawler</span> <span class="o">=</span> <span class="n">WebCrawler</span><span class="p">(</span><span class="n">cache_dir</span><span class="o">=</span><span class="s2">&#34;.cache/crawl&#34;</span><span class="p">,</span> <span class="n">max_workers</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="o">=</span> <span class="n">CrawlScope</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">roots</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;https://example.com/docs&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">include_patterns</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;https://example.com/docs/**&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">depth</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">documents</span> <span class="o">=</span> <span class="n">crawler</span><span class="o">.</span><span class="n">markdown_documents</span><span class="p">(</span><span class="n">scope</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">DuckDBStore</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">location</span><span class="o">=</span><span class="s2">&#34;raghilda.duckdb&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">embed</span><span class="o">=</span><span class="n">EmbeddingOpenAI</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">overwrite</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">summary</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">ingest</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">documents</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">prepare</span><span class="o">=</span><span class="n">MarkdownChunker</span><span class="p">(</span><span class="n">chunk_size</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span><span class="o">.</span><span class="n">chunk</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_workers</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">summary</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>IngestSummary(inserted=142, replaced=0, skipped=0)
</code></pre>
<p>The <code>CrawlScope</code> dataclass defines the traversal policy: root URLs,
include/exclude patterns, depth limits, and page count caps. The crawler
handles the mechanics of fetching and converting pages to Markdown,
while the scope tells it where to go. This separation means you can
change the backend (swap <code>WebCrawler</code> for <code>CloudflareCrawler</code>, for
instance) without redefining the scope, and vice versa.</p>
<p>Three concrete crawlers ship with <code>v0.2</code>. <code>DirectoryCrawler</code> walks local
file trees and converts supported formats to Markdown. <code>WebCrawler</code>
fetches pages over HTTP using <code>requests</code> and converts them locally.
<code>CloudflareCrawler</code> delegates both fetching and rendering to
Cloudflare&rsquo;s Browser Rendering API (the right tool for sites that load
content through JavaScript). All three implement the same interface:
<code>origins()</code> to discover pages, <code>fetch_raw()</code> and <code>fetch_markdown()</code> for
single-page access, and <code>markdown_documents()</code> for the full pipeline.</p>
<p>Caching is built into the crawler layer. When you pass <code>cache_dir=True</code>
(or an explicit path), each crawler stores fetched content and converted
Markdown in a flat directory of files with metadata sidecars. On
subsequent runs, cached entries are reused if they are still fresh. For
<code>WebCrawler</code> and <code>CloudflareCrawler</code>, freshness is controlled by
<code>cache_stale_after=</code>, a <code>timedelta</code> that defines how long a cached entry
remains valid. This makes interrupted workflows resumable without any
explicit checkpoint logic: rerun the script and the cache supplies
everything that was already fetched, while only new or stale pages
trigger network requests.</p>
<p>Concurrency operates on both sides of this boundary independently. The
crawler can fetch and convert pages in parallel (controlled by
<code>max_workers=</code> on the crawler constructor), and <code>store.ingest()</code> can
write to the store concurrently (controlled by its own <code>max_workers=</code>
argument). For <code>WebCrawler</code>, the breadth-first frontier is explored
concurrently while preserving stable output order, so results come back
in a consistent sequence regardless of which pages respond first.</p>
<h2 id="cloudflarecrawler"><code>CloudflareCrawler</code>
</h2>
<p>The <code>CloudflareCrawler</code> moves the work of a crawl off the local machine.
Instead of fetching pages and converting them to Markdown with local
processes, it hands both jobs to Cloudflare&rsquo;s Browser Rendering API, so
a long crawl over a large site runs on Cloudflare&rsquo;s distributed
infrastructure rather than competing for local CPU and bandwidth. For
collections large enough that concurrent local requests become the
bottleneck, this is the primary reason to reach for it: the slow,
sustained part of building a store happens remotely, and what returns is
ready-to-chunk Markdown. The same arrangement resolves a problem that
defeats a plain HTTP fetch, because the API renders each page in a real
browser, executing JavaScript and waiting for the DOM to settle before
extracting content. Sites built with React, Vue, or Angular, which an
ordinary request reduces to an empty shell, are therefore handled
without extra configuration or a locally installed headless browser.</p>
<p>The usage looks almost identical to <code>WebCrawler</code>, because both share the
same crawl interface. The key difference is that the constructor takes
Cloudflare credentials instead of an HTTP session, and Cloudflare&rsquo;s
infrastructure handles the rendering remotely (so there is no need to
install Playwright, Selenium, or any other local headless browser):</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.crawl</span> <span class="kn">import</span> <span class="n">CloudflareCrawler</span><span class="p">,</span> <span class="n">CrawlScope</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">crawler</span> <span class="o">=</span> <span class="n">CloudflareCrawler</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">account_id</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;CLOUDFLARE_ACCOUNT_ID&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">api_token</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;CLOUDFLARE_API_TOKEN&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">cache_dir</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">render</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_workers</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="o">=</span> <span class="n">CrawlScope</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">roots</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;https://my-spa-docs.example.com/&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">depth</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">include_patterns</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;https://my-spa-docs.example.com/**&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">limit</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">documents</span> <span class="o">=</span> <span class="n">crawler</span><span class="o">.</span><span class="n">markdown_documents</span><span class="p">(</span><span class="n">scope</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Iterating the result performs the crawl lazily, yielding one
<code>MarkdownDocument</code> per page. Each document exposes the page&rsquo;s <code>origin</code>
alongside its rendered Markdown <code>content</code>, so a quick pass confirms that
the JavaScript-rendered pages came back with real text rather than the
empty shells a plain HTTP fetch would have produced:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">documents</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">origin</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;(</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">content</span><span class="p">)</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2"> chars)&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>https://my-spa-docs.example.com/ (3,214 chars)
https://my-spa-docs.example.com/guide/install (5,902 chars)
https://my-spa-docs.example.com/guide/config (8,477 chars)
https://my-spa-docs.example.com/api/reference (12,043 chars)
</code></pre>
<p>The <code>render=True</code> default tells Cloudflare to execute JavaScript before
extracting content. For server-rendered sites where JavaScript execution
is unnecessary, setting <code>render=False</code> reduces crawl time and API usage.
The <code>source=</code> parameter controls how pages are discovered: <code>&quot;all&quot;</code> (the
default) combines multiple discovery methods, <code>&quot;sitemap&quot;</code> reads from the
site&rsquo;s <code>sitemap.xml</code>, <code>&quot;crawl&quot;</code> follows links from the rendered DOM, and
<code>&quot;urls&quot;</code> processes only the explicitly provided roots.</p>
<p>For stores that need regular updates, the <code>modified_since=</code> parameter
restricts the crawl to pages modified after a given Unix timestamp,
keeping refresh jobs lightweight. Combined with the crawl cache and the
store&rsquo;s own deduplication (identical documents are not re-embedded), an
incremental update script can run daily without redundant work:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">one_week_ago</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">())</span> <span class="o">-</span> <span class="p">(</span><span class="mi">7</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">crawler</span> <span class="o">=</span> <span class="n">CloudflareCrawler</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">account_id</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;CLOUDFLARE_ACCOUNT_ID&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">api_token</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;CLOUDFLARE_API_TOKEN&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">cache_dir</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">cache_stale_after</span><span class="o">=</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">modified_since</span><span class="o">=</span><span class="n">one_week_ago</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>The tradeoff is cost: <code>CloudflareCrawler</code> requires a Cloudflare account
with Browser Rendering access. For static HTML sites where a plain HTTP
fetch returns the full content, <code>WebCrawler</code> remains the simpler and
free option. Both crawlers share the same interface, so switching
between them requires only a constructor change.</p>
<h2 id="postgresql-store">PostgreSQL store
</h2>
<p>raghilda <code>v0.1</code> shipped with three store backends: DuckDB (local,
zero-config), ChromaDB, and OpenAI Vector Stores. <code>v0.2</code> adds
<code>PostgreSQLStore</code>, backed by <code>psycopg2</code> and <code>pgvector</code>. This is the
natural choice for production deployments where the store needs to be
shared across services, or where you already have PostgreSQL
infrastructure.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.store</span> <span class="kn">import</span> <span class="n">PostgreSQLStore</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.embedding</span> <span class="kn">import</span> <span class="n">EmbeddingOpenAI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">PostgreSQLStore</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">connection</span><span class="o">=</span><span class="s2">&#34;postgresql://user:pass@localhost:5432/mydb&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">embed</span><span class="o">=</span><span class="n">EmbeddingOpenAI</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="o">=</span><span class="s2">&#34;docs_store&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">overwrite</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>The store supports full-text search via PostgreSQL&rsquo;s native
<code>tsvector</code>/<code>tsquery</code> with a pre-computed column and GIN index, vector
similarity search via pgvector with HNSW indexes (supporting cosine, L2,
and inner product distance metrics), and combined retrieval that merges
both result sets with deoverlap support.</p>
<p>Retrieval uses the same interface as every other backend: a single
<code>retrieve()</code> call returns a ranked list of chunks, each carrying its
similarity score under <code>metrics</code> and its heading-hierarchy <code>context</code>.
Running a query against a store populated with the raghilda
documentation returns the most relevant chunks first:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">retrieve</span><span class="p">(</span><span class="s2">&#34;How are vector indexes configured?&#34;</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Score: </span><span class="si">{</span><span class="n">r</span><span class="o">.</span><span class="n">metrics</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">value</span><span class="si">:</span><span class="s2">.4f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;---&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>Score: 0.5142
# PostgreSQL store
Vector similarity search runs through pgvector with HNSW indexes,
supporting cosine, L2, and inner product distance metrics. Call
build_index() after ingestion to create them.
---
Score: 0.4417
# PostgreSQL store &gt; Combined retrieval
Combined retrieval merges the vector and full-text result sets and
applies deoverlap to drop redundant overlapping chunks, returning a
single ranked list from one retrieve() call.
---
</code></pre>
<p>Attributes work as expected: scalar types map to columns, struct types
map to JSONB, and attribute filters can query into JSONB fields using
the <code>-&gt;&gt;</code> operator. The <code>build_index()</code> method creates HNSW indexes
after ingestion, and the <code>vss_index=</code> parameter on <code>create()</code> controls
the default index type.</p>
<p>Connection strings are accepted directly in <code>create()</code> and <code>connect()</code>,
so you can point the store at any PostgreSQL instance with pgvector
installed. If the pgvector extension is missing, the store raises an
informative error rather than failing cryptically on the first vector
operation.</p>
<h2 id="nvidia-nim-embeddings">NVIDIA NIM embeddings
</h2>
<p>The embedding layer gains a new provider: <code>EmbeddingNVIDIA</code>, which
connects to NVIDIA&rsquo;s OpenAI-compatible embedding API. The default model
is <code>nvidia/llama-nemotron-embed-1b-v2</code>, a compact embedding model
suitable for retrieval workloads. The provider reads its API key from
the <code>NVIDIA_API_KEY</code> environment variable.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.embedding</span> <span class="kn">import</span> <span class="n">EmbeddingNVIDIA</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">embedding</span> <span class="o">=</span> <span class="n">EmbeddingNVIDIA</span><span class="p">()</span></span></span></code></pre></div></div>
<p>One notable feature of NVIDIA&rsquo;s embedding API is differentiated input
types: queries and documents are embedded with different prefixes
(<code>&quot;query&quot;</code> and <code>&quot;passage&quot;</code>), which can improve retrieval quality for
asymmetric search where the query is short and the documents are long.
raghilda handles this distinction automatically when the store calls the
embedding provider during ingestion and retrieval.</p>
<p>The provider includes built-in rate limit handling with exponential
backoff. NVIDIA&rsquo;s 429 responses carry no <code>Retry-After</code> header or rate
limit metadata, so backoff is the only viable strategy. For users who
want to keep embedding computation entirely local, NVIDIA NIM can also
be self-hosted, in which case you point the provider at your local
endpoint with a <code>base_url=</code> override.</p>
<h2 id="improved-duckdb-error-messages">Improved DuckDB error messages
</h2>
<p>A smaller but practical improvement: <code>DuckDBStore</code> now raises clear,
actionable errors when BM25 retrieval is attempted before the index has
been built, or after writes have made the index stale. Previously, this
produced a cryptic <code>CatalogException</code> from DuckDB about a missing
<code>match_bm25</code> function. The new message tells you exactly what to do:</p>
<pre><code>RuntimeError: DuckDBStore retrieval requires a current BM25 index.
Call `store.build_index(&quot;bm25&quot;)` after inserting or updating documents
and before calling `retrieve_bm25()` or `retrieve()`.
</code></pre>
<p>The store now tracks BM25 freshness internally: the index is marked
stale after any <code>upsert()</code> call and marked current after
<code>build_index(&quot;bm25&quot;)</code>. This tracking happens off the retrieval hot path,
so there is no per-query overhead. HNSW indexes are unaffected because
DuckDB maintains them across writes automatically.</p>
<h2 id="why-even-use-raghilda">Why even use raghilda?
</h2>
<p>raghilda is a retrieval library, not an orchestration framework. Larger
projects like LangChain and LlamaIndex offer composable retrieval
components too, but they also ship agent runtimes, chain abstractions,
prompt management, and memory systems. If all you need is the retrieval
pipeline (crawl, chunk, embed, store, retrieve), raghilda gives you that
without the surrounding framework. The API surface is small: plain
dataclasses, iterators, and direct function calls. There are fewer
layers of indirection between your code and the underlying operations,
which makes the pipeline easier to debug and reason about.</p>
<p>raghilda <code>v0.2</code> makes that focused scope practical at scale. The crawl
API adds caching and concurrency while keeping each step a separate,
inspectable call. The storage layer lets you start with a local DuckDB
file and move to PostgreSQL or OpenAI Vector Stores later without
changing retrieval code. And every backend provides hybrid retrieval
(semantic search, BM25, and attribute filtering combined in a single
<code>retrieve()</code> call) out of the box, without assembling separate retriever
classes or configuring a pipeline graph.</p>
<h2 id="getting-started">Getting started
</h2>
<p>raghilda <code>v0.2</code> is available now on PyPI (<code>pip install raghilda</code>). The
<a href="https://posit-dev.github.io/raghilda/" target="_blank" rel="noopener">raghilda documentation site</a>
covers all of the features described here in more detail. The <a href="https://posit-dev.github.io/raghilda/user-guide/getting-started.html" target="_blank" rel="noopener">Getting
Started</a>
guide walks through building a store from scratch, and the <a href="https://posit-dev.github.io/raghilda/user-guide/crawling-and-ingestion.html" target="_blank" rel="noopener">Crawling and
Ingestion</a>
guide covers the new crawl API in depth. A dedicated
<a href="https://posit-dev.github.io/raghilda/user-guide/cloudflare-crawler.html" target="_blank" rel="noopener">CloudflareCrawler</a>
guide explains browser rendering, page discovery, caching, and
incremental updates. The <a href="https://github.com/posit-dev/raghilda" target="_blank" rel="noopener">GitHub
repository</a> has the source, issue
tracker, and full changelog. If you run into problems or have feature
requests, open an issue there.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-01_raghilda-0-2-0/assets/raghilda-updated.png" length="1954675" type="image/png" />
    </item>
    <item>
      <title>watcher 0.2.0: filesystem watching for R, and the engine behind Shiny auto-reload</title>
      <link>https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/</link>
      <pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<p>If you&rsquo;ve ever saved a file in your Shiny app and watched the browser refresh on its own, you may have already used <a href="https://watcher.r-lib.org" target="_blank" rel="noopener">watcher</a> without knowing it.</p>
<p>watcher is a lightweight R package that watches files and directories for changes and reacts in the background. It shipped quietly last year as the engine behind Shiny&rsquo;s auto-reload, and until now that is mostly where it lived. watcher 0.2.0 is on CRAN, and we&rsquo;re taking this opportunity to introduce it as a general-purpose filesystem watcher for R developers to use.</p>
<p>Install it from CRAN:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;watcher&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="the-engine-behind-shiny-auto-reload">The engine behind Shiny auto-reload
</h2>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/editor-and-app.png"
      alt="A code editor showing an app.R file on the left and the running Shiny app on the right: an &ldquo;Old Faithful Eruptions&rdquo; histogram with a &ldquo;Number of bins&rdquo; slider." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The inner loop of building a Shiny app is edit, save, switch to the browser, reload. Auto-reload removes the friction of the last two steps: you save, and the app reloads itself.</p>
<p>The easiest way to turn it on is Shiny&rsquo;s <a href="https://shiny.posit.co/r/reference/shiny/latest/devmode.html" target="_blank" rel="noopener">Developer Mode</a>, which flips on a handful of developer-friendly options for the session – auto-reload among them, alongside unminified JavaScript and full stack traces. Call <code>devmode()</code> once and run your app as usual:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">shiny</span><span class="o">::</span><span class="nf">devmode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">shiny</span><span class="o">::</span><span class="nf">runApp</span><span class="p">(</span><span class="s">&#34;app.R&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Now every time you save a file in the app directory, the running app reloads to match:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/shiny-autoreload.gif"
      alt="A code editor and a running Shiny app side by side. As lines in app.R are edited and saved – the title, the bar colour, the number of bins – the app reloads to match, with no interaction in the browser." 
      loading="lazy"
    >
  </figure></div>
</p>
<p><em>Editing <code>app.R</code> on the left; the app on the right reloads on each save. No manual refresh or restart.</em></p>
<p>Under the hood, Developer Mode sets <code>shiny.autoreload = TRUE</code> (you can set that option directly if you&rsquo;d rather not switch on the rest of Developer Mode), which hands your app directory to watcher and starts it watching in the background. The instant you save a file, watcher&rsquo;s callback fires and Shiny pushes a reload to the browser.</p>
<p>Previously Shiny did this by polling: every few hundred milliseconds it re-listed the directory and compared modification times. That works, but the cost scales with the size of your project and the reload only ever happens on the next tick. watcher instead subscribes to the operating system&rsquo;s own filesystem-change notifications, so the reload fires on the save itself, and an idle app does no work at all.</p>
<p>For version 0.2.0, we&rsquo;ve simplified how the package installs from source. This means we can have it power auto-reload by default: the next release of Shiny will require watcher outright, so it&rsquo;s installed alongside Shiny with nothing to install by hand. (Shiny 1.14.0 uses watcher when it&rsquo;s present and falls back to polling otherwise – so for now it&rsquo;s worth installing watcher yourself).</p>
<h2 id="watcher-the-package">watcher, the package
</h2>
<p>This same machinery is available directly, outside of Shiny. <code>watcher()</code> returns an <a href="https://r6.r-lib.org" target="_blank" rel="noopener">R6</a> object that you can start and stop:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">watcher</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dir.create</span><span class="p">(</span><span class="s">&#34;data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">w</span> <span class="o">&lt;-</span> <span class="nf">watcher</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="s">&#34;data&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">callback</span> <span class="o">=</span> <span class="nf">\</span><span class="p">(</span><span class="n">paths</span><span class="p">)</span> <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;changed:&#34;</span><span class="p">,</span> <span class="n">paths</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">latency</span> <span class="o">=</span> <span class="m">0.5</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">w</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span></span></span></code></pre></div></div>
<p>From now on, any change under <code>data/</code> calls your function back with the paths that changed:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">file.create</span><span class="p">(</span><span class="s">&#34;data/report.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; changed: /home/you/project/data/report.csv</span></span></span></code></pre></div></div>
<p>Three arguments to <code>watcher()</code> cover most needs:</p>
<ul>
<li><strong><code>path</code></strong> – a file, a directory (watched recursively), or a vector of paths. Defaults to the working directory.</li>
<li><strong><code>callback</code></strong> – a function taking one argument: a character vector of the paths that changed. The default, <code>NULL</code>, simply writes the changed paths to <code>stdout</code>.</li>
<li><strong><code>latency</code></strong> – seconds to debounce events before reporting them. Defaults to 1.</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">w</span><span class="o">$</span><span class="nf">stop</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The returned object also has <code>$stop()</code>, <code>$is_running()</code> and <code>$get_path()</code> alongside <code>$start()</code>.</p>
<p>watcher runs on a background thread, but your callback runs on R&rsquo;s main thread, scheduled through <a href="https://later.r-lib.org" target="_blank" rel="noopener">later</a>. It fires when R is idle at the top level, whenever you call <code>later::run_now()</code>, or automatically inside an event loop such as Shiny&rsquo;s. This is what lets it slot easily into Shiny or plumber without blocking the session.</p>
<h2 id="put-it-to-work">Put it to work
</h2>
<p>watcher is deliberately small and general. Anything you want to happen when a file changes, you can wire to it:</p>
<ul>
<li>rebuild a report or re-render a document when its source or data changes</li>
<li>reprocess a directory as new files land in it</li>
<li>re-run tests or reload package code while you develop</li>
<li>reload configuration without restarting a long-running service</li>
</ul>
<p>The shape is always the same – give <code>watcher()</code> a path and a function:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">watcher</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rebuild</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">paths</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">message</span><span class="p">(</span><span class="nf">format</span><span class="p">(</span><span class="nf">Sys.time</span><span class="p">()),</span> <span class="s">&#34;: &#34;</span><span class="p">,</span> <span class="nf">length</span><span class="p">(</span><span class="n">paths</span><span class="p">),</span> <span class="s">&#34; file(s) changed&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ... your render / test / reload step here ...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">w</span> <span class="o">&lt;-</span> <span class="nf">watcher</span><span class="p">(</span><span class="s">&#34;data&#34;</span><span class="p">,</span> <span class="n">rebuild</span><span class="p">,</span> <span class="n">latency</span> <span class="o">=</span> <span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">w</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The callback receives the paths that changed, so you can act on exactly what triggered it. File created, updated, removed and renamed events are monitored, for individual files or whole directory trees.</p>
<p>From here:</p>
<ul>
<li><a href="https://watcher.r-lib.org" target="_blank" rel="noopener">watcher package site</a> – reference and examples.</li>
<li><a href="https://github.com/r-lib/watcher" target="_blank" rel="noopener">r-lib/watcher</a> – source, issues, and feedback.</li>
<li>For Shiny, enable Developer Mode with <code>shiny::devmode()</code> (or set <code>options(shiny.autoreload = TRUE)</code> directly).</li>
</ul>
<h2 id="acknowledgments">Acknowledgments
</h2>
<p>watcher builds on <a href="https://github.com/emcrisostomo/fswatch" target="_blank" rel="noopener">libfswatch</a> by Enrico M. Crisostomo and Alan Dipert. This is a mature C++ filesystem-monitoring library, which uses the native, event-driven notification API on each platform.</p>
<p>Thanks go to Garrick Aden-Buie for wiring watcher into Shiny&rsquo;s auto-reload, and to everyone who has contributed issues, fixes, and feedback.</p>
<p>Questions and ideas are very welcome on GitHub: <a href="https://github.com/r-lib/watcher/issues" target="_blank" rel="noopener">r-lib/watcher</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-06-29_watcher-0-2-0/featured.jpg" length="298578" type="image/jpeg" />
    </item>
    <item>
      <title>Air 0.10.0</title>
      <link>https://opensource.posit.co/blog/2026-06-26_air-0-10-0/</link>
      <pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-06-26_air-0-10-0/</guid>
      <dc:creator>Davis Vaughan</dc:creator><description><![CDATA[<p>We&rsquo;re very excited to announce that Air 0.10.0 is out now!
<a href="https://github.com/posit-dev/air" target="_blank" rel="noopener">Air</a> is an extremely fast R code formatter, capable of styling entire projects in the blink of an eye.
We haven&rsquo;t done a release post in awhile, so this one will serve as a round up of everything from 0.8.2 to 0.10.0.</p>
<p>If you use <a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a>, the Air Extension has probably already updated you to the new release!
Otherwise, follow one of Air&rsquo;s <a href="https://posit-dev.github.io/air/editors.html" target="_blank" rel="noopener">editor guides</a> to install the latest version for your editor.</p>
<p>To see every bug fix, check out our <a href="https://github.com/posit-dev/air/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CLI changelog</a> and <a href="https://github.com/posit-dev/air/blob/main/editors/code/CHANGELOG.md" target="_blank" rel="noopener">extension changelog</a>.</p>
<h2 id="assignment-style-">Assignment style 😎
</h2>
<p>The headlining feature of 0.10.0 is a new <code>assignment-style</code> option, one of our <a href="https://github.com/posit-dev/air/issues/359" target="_blank" rel="noopener">top requests</a>!
This allows you to enforce <code>&lt;-</code> or <code>=</code> for assignment throughout your codebase.
Choose one of <code>&quot;arrow&quot;</code> for <code>&lt;-</code>, <code>&quot;equal&quot;</code> for <code>=</code>, or <code>&quot;preserve&quot;</code> to leave existing code as is.
For example, with <code>assignment-style = &quot;arrow&quot;</code>:</p>
<!-- panache-ignore-format-start -->
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># This</span>
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="n">fn</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">z</span> <span class="o">&lt;-</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">  <span class="n">z</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Is standardized to</span>
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">&lt;-</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="n">fn</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">z</span> <span class="o">&lt;-</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">  <span class="n">z</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<!-- panache-ignore-format-end -->
<p>This option defaults to enforcing <code>&lt;-</code> everywhere, which is what the <a href="https://style.tidyverse.org/syntax.html#assignment-1" target="_blank" rel="noopener">style guide</a> already recommended.
Note that this is a breaking change for Air, as the previous behavior didn&rsquo;t enforce any assignment style.
To revert to the old behavior, use <code>&quot;preserve&quot;</code>.</p>
<p>This feature is surprisingly sophisticated!
In R, you can always convert from <code>=</code> to <code>&lt;-</code> without changing the meaning of the code, but the reverse isn&rsquo;t true.
Consider the following:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">quote</span><span class="p">(</span><span class="n">x</span> <span class="o">&lt;-</span> <span class="m">1</span><span class="p">)</span></span></span></code></pre></div></div>
<p>This quotes the expression <code>x &lt;- 1</code> and returns it.
Blindly changing to <code>=</code> results in:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">quote</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="m">1</span><span class="p">)</span></span></span></code></pre></div></div>
<p>That&rsquo;s very different!
This sets an argument named <code>x</code> to <code>1</code>.
Trying to run this will result in an error since <code>quote()</code> doesn&rsquo;t have an <code>x</code> argument.
Air knows every case where <code>&lt;-</code> is allowed but <code>=</code> isn&rsquo;t, so you can rest assured that this option won&rsquo;t impact the meaning of your code in any way.</p>
<h2 id="positron-terminal-support">Positron terminal support
</h2>
<p>Positron&rsquo;s integration with Air has gotten even better!
When the Air extension activates, it now prepends the bundled Air executable to your integrated terminal&rsquo;s <code>PATH</code>.
This means:</p>
<ul>
<li>You can now run Air commands from Positron&rsquo;s terminal, even without installing the CLI separately.</li>
<li>Your terminal version of Air should now always match the version used by the editor itself.</li>
</ul>
<div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/assets/which-air.mp4"
        class="w-full h-full object-contain"
        title="Air in your Positron terminal"
        controls></video>
    </div>
<p>This is particularly useful with agents like Claude Code, where you&rsquo;ll often want to tell your agent to run Air on any R files it touches (see <code>usethis::use_tidy_agents()</code> in the development version of usethis for a way to automatically add this to an <code>AGENTS.md</code>).</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/assets/air-claude.png"
      alt="Claude Code finding Air" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>If you&rsquo;ve already installed the Air CLI separately, note that the bundled version will take precedence in Positron&rsquo;s terminals by default.
To force both the editor and the integrated terminal to use your externally installed version of Air, set <code>air.executableStrategy: &quot;environment&quot;</code>.
To opt-out of terminal support, set <code>air.addExecutableToTerminalPath: false</code>.</p>
<h2 id="rstudio-improvements">RStudio improvements
</h2>
<p>Thanks to <a href="https://github.com/kevinushey" target="_blank" rel="noopener">Kevin Ushey</a>, RStudio&rsquo;s support for Air is improving rapidly!</p>
<blockquote>
<p>Note that you&rsquo;ll need RStudio 2026.06.0 for this, which isn&rsquo;t out yet, but should be by the end of next week.</p>
</blockquote>
<p>RStudio now has &ldquo;native&rdquo; support for Air, rather than just being configurable as an External Formatter.
If you opt-in to Air, RStudio will now automatically download the latest version of the <code>air</code> binary the first time you save a file, making the whole experience much smoother.</p>
<div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/assets/air-download.mp4"
        class="w-full h-full object-contain"
        title="Air auto downloading in RStudio"
        controls></video>
    </div>
<p>You can turn this on via <code>Tools -&gt; Global Options... -&gt; Code -&gt; Formatting</code>.
I recommend checking both <code>Use Air for code formatting</code> and <code>Reformat documents on save</code>.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/assets/air-rstudio-settings.png"
      alt="RStudio settings for Air" 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="lionstigersandbears-uv-pixi-and-mise-oh-my"><del>Lions, tigers, and bears</del> uv, pixi, and mise, oh my!
</h2>
<p>Air now lives on PyPi as <a href="https://pypi.org/project/air-formatter/" target="_blank" rel="noopener"><code>air-formatter</code></a> (sadly <code>air</code> was already taken), which means that you can now install Air&rsquo;s CLI via <a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener">uv</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install globally</span>
</span></span><span class="line"><span class="cl">uv tool install air-formatter
</span></span><span class="line"><span class="cl">air format path/to/file.R
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run one-off command</span>
</span></span><span class="line"><span class="cl">uvx --from air-formatter air format path/to/file.R</span></span></code></pre></div></div>
<p>It might sound a little absurd to put an R code formatting tool on Python&rsquo;s PyPI, but in a world where an increasingly large number of R users are also uv users, it&rsquo;s pretty darn convenient to be able to invoke it this way!</p>
<p>Additionally, Air is on <a href="https://github.com/conda-forge/air-feedstock" target="_blank" rel="noopener">conda-forge</a> thanks to <a href="https://github.com/salim-b" target="_blank" rel="noopener"><code>@salim-b</code></a>, which means you can also install it with <a href="https://pixi.prefix.dev/latest/" target="_blank" rel="noopener">pixi</a> and <a href="https://mise.jdx.dev/" target="_blank" rel="noopener">mise</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add to a project</span>
</span></span><span class="line"><span class="cl">pixi add air
</span></span><span class="line"><span class="cl">mise use conda:air
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install globally</span>
</span></span><span class="line"><span class="cl">pixi global install air
</span></span><span class="line"><span class="cl">mise use --global conda:air
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run one-off command</span>
</span></span><span class="line"><span class="cl">pixi <span class="nb">exec</span> air format path/to/my/script.R
</span></span><span class="line"><span class="cl">mise <span class="nb">exec</span> conda:air -- air format path/to/my/script.R</span></span></code></pre></div></div>
<h2 id="pre-commit-support">pre-commit support
</h2>
<p>Air now has <a href="https://pre-commit.com/" target="_blank" rel="noopener">pre-commit</a> and <a href="https://prek.j178.dev/" target="_blank" rel="noopener">prek</a> support via <a href="https://github.com/posit-dev/air-pre-commit" target="_blank" rel="noopener">posit-dev/air-pre-commit</a>.
To run Air on changed R files before every commit, add the following to your <code>.pre-commit-config.yaml</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">repos</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">repo</span><span class="p">:</span><span class="w"> </span><span class="l">https://github.com/posit-dev/air-pre-commit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Air version</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rev</span><span class="p">:</span><span class="w"> </span><span class="m">0.10.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hooks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Run the formatter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">air-format</span></span></span></code></pre></div></div>
<h2 id="stdin-support">stdin support
</h2>
<p>Air&rsquo;s CLI now has <a href="https://posit-dev.github.io/air/cli.html#stdin" target="_blank" rel="noopener">stdin support</a>!
You can activate this with the new <code>--stdin-file-path</code> option like so:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat R/across.R <span class="p">|</span> air format --stdin-file-path R/across.R</span></span></code></pre></div></div>
<p><code>air</code> will receive the contents of <code>cat R/across.R</code> on stdin, format it, and then emit the formatted result back out on stdout.</p>
<p>You typically won&rsquo;t call this directly, but it&rsquo;s very useful for editors and IDEs without full language server support.
For example, with <a href="https://zed.dev/" target="_blank" rel="noopener">Zed</a> you can configure Air as an external formatter with the following setup:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;formatter&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;external&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;air&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;arguments&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;format&#34;</span><span class="p">,</span> <span class="s2">&#34;--stdin-file-path&#34;</span><span class="p">,</span> <span class="s2">&#34;{buffer_path}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>This allows Zed to invoke Air on every file save.</p>
<blockquote>
<p>Note that Zed does have full language server support, and it&rsquo;s recommended to use our <a href="https://posit-dev.github.io/air/editor-zed.html" target="_blank" rel="noopener">Air extension for Zed</a> instead, but it makes for a nice example anyways!</p>
</blockquote>
<p>Invoking Air via stdin can also be useful in some embedded contexts, such as inside <a href="https://github.com/eitsupi/arf" target="_blank" rel="noopener">arf</a>, an R console.</p>
<h2 id="shell-completions">Shell completions
</h2>
<p>Thanks to <a href="https://github.com/salim-b" target="_blank" rel="noopener"><code>@salim-b</code></a>, Air can now generate shell completions for itself via a new <code>air generate-shell-completion &lt;shell&gt;</code> command, where <code>&lt;shell&gt;</code> can be one of <code>zsh</code>, <code>bash</code>, <code>powershell</code>, <code>fish</code>, or <code>elvish</code>.
Read the <a href="https://posit-dev.github.io/air/cli.html#shell-completions" target="_blank" rel="noopener">documentation</a> for how to set this up for your specific shell.
With shell completions set up, you can <code>Tab</code> your way through <code>air</code> commands at the command line:</p>
<div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/assets/air-completions.mp4"
        class="w-full h-full object-contain"
        title="Air shell completions"
        controls></video>
    </div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-06-26_air-0-10-0/featured.jpg" length="638589" type="image/jpeg" />
    </item>
    <item>
      <title>CatBoost support in tidymodels</title>
      <link>https://opensource.posit.co/blog/2026-06-25_catboost-tidymodels/</link>
      <pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-06-25_catboost-tidymodels/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p><a href="https://catboost.ai/" target="_blank" rel="noopener">CatBoost</a> is a very popular and high-quality gradient boosting library.
With the latest releases of <a href="https://bonsai.tidymodels.org/" target="_blank" rel="noopener">bonsai</a> and <a href="https://parsnip.tidymodels.org/" target="_blank" rel="noopener">parsnip</a>,
you can now train CatBoost models from R using the same tidymodels interface you already use for xgboost, LightGBM, and the rest of the <code>boost_tree()</code> family.</p>
<h2 id="installing-catboost">Installing CatBoost
</h2>
<p>The one wrinkle is installation.
The CatBoost R package is not on CRAN,
so you can&rsquo;t reach for <code>install.packages(&quot;catboost&quot;)</code> directly.</p>
<p>Grab the URL for your platform from the <a href="https://catboost.ai/docs/en/installation/r-installation-binary-installation" target="_blank" rel="noopener">CatBoost R installation guide</a> and install it with the remotes package.
For example, on an Apple Silicon or Intel Mac:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;remotes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">remotes</span><span class="o">::</span><span class="nf">install_url</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;https://github.com/catboost/catboost/releases/download/v1.2.10/catboost-R-darwin-universal2-1.2.10.tgz&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">INSTALL_opts</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;--no-multiarch&#34;</span><span class="p">,</span> <span class="s">&#34;--no-test-load&#34;</span><span class="p">,</span> <span class="s">&#34;--no-staged-install&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>Swap in the release version, operating system, and architecture that match your setup.
The guide lists the full URL pattern and the binaries available for each release.</p>
<p>Once CatBoost itself is installed,
the tidymodels packages is just the usual packages:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># install.packages(&#34;pak&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;tidymodels&#34;</span><span class="p">,</span> <span class="s">&#34;bonsai&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
<p>You&rsquo;ll need <strong>bonsai 0.4.1</strong> (or later) and <strong>parsnip 1.4.0</strong> (or later),
which is where the CatBoost engine landed and got polished.</p>
<h2 id="fitting-a-catboost-model">Fitting a CatBoost model
</h2>
<p>CatBoost is supported as an engine for []<code>boost_tree()</code>](<a href="https://parsnip.tidymodels.org/reference/details_boost_tree_catboost.html%29" target="_blank" rel="noopener">https://parsnip.tidymodels.org/reference/details_boost_tree_catboost.html)</a>.
Loading bonsai registers the engine,
and from there it behaves like any other parsnip model spec:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bonsai</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cat_spec</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">boost_tree</span><span class="p">(</span><span class="n">trees</span> <span class="o">=</span> <span class="m">500</span><span class="p">,</span> <span class="n">learn_rate</span> <span class="o">=</span> <span class="m">0.05</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;catboost&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;regression&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cat_fit</span> <span class="o">&lt;-</span> <span class="nf">fit</span><span class="p">(</span><span class="n">cat_spec</span><span class="p">,</span> <span class="n">mpg</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cat_fit</span></span></span></code></pre></div></div>
<pre><code>parsnip model object

CatBoost model (500 trees)
Loss function: RMSE
Fit to 10 feature(s)
</code></pre>
<h2 id="tuning">Tuning
</h2>
<p>The CatBoost engine supports the standard <code>boost_tree()</code> tuning parameters,
and the recent releases made tuning both faster and more correct.</p>
<p>A typical tuning setup looks like this:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">cat_spec</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">boost_tree</span><span class="p">(</span><span class="n">trees</span> <span class="o">=</span> <span class="nf">tune</span><span class="p">(),</span> <span class="n">learn_rate</span> <span class="o">=</span> <span class="nf">tune</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;catboost&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;regression&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cat_wf</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">cat_spec</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">123</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">folds</span> <span class="o">&lt;-</span> <span class="nf">vfold_cv</span><span class="p">(</span><span class="n">mtcars</span><span class="p">,</span> <span class="n">v</span> <span class="o">=</span> <span class="m">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tune_res</span> <span class="o">&lt;-</span> <span class="nf">tune_grid</span><span class="p">(</span><span class="n">cat_wf</span><span class="p">,</span> <span class="n">resamples</span> <span class="o">=</span> <span class="n">folds</span><span class="p">,</span> <span class="n">grid</span> <span class="o">=</span> <span class="m">20</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">show_best</span><span class="p">(</span><span class="n">tune_res</span><span class="p">,</span> <span class="n">metric</span> <span class="o">=</span> <span class="s">&#34;rmse&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 5 × 8
  trees learn_rate .metric .estimator  mean     n std_err .config         
  &lt;int&gt;      &lt;dbl&gt; &lt;chr&gt;   &lt;chr&gt;      &lt;dbl&gt; &lt;int&gt;   &lt;dbl&gt; &lt;chr&gt;           
1  1473     0.0379 rmse    standard    2.76     5   0.419 pre0_mod15_post0
2   527     0.0513 rmse    standard    2.81     5   0.443 pre0_mod06_post0
3  1053     0.0941 rmse    standard    2.83     5   0.447 pre0_mod11_post0
4   211     0.127  rmse    standard    2.84     5   0.400 pre0_mod03_post0
5   632     0.234  rmse    standard    2.86     5   0.480 pre0_mod07_post0
</code></pre>
<p>Thanks to the <a href="https://parsnip.tidymodels.org/articles/Submodels.html" target="_blank" rel="noopener">submodel trick</a>
tuning <code>trees</code> doesn&rsquo;t require refitting the model from scratch at every candidate value.</p>
<h2 id="orbital-support">orbital support
</h2>
<p>CatBoost models also work with <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a>,
allowing you to turn your fitted catboost model into SQL and run predictions directly inside a database.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">orbital</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cat_spec</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">boost_tree</span><span class="p">(</span><span class="n">trees</span> <span class="o">=</span> <span class="m">50</span><span class="p">,</span> <span class="n">learn_rate</span> <span class="o">=</span> <span class="m">0.05</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;catboost&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;regression&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cat_wf</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">cat_spec</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cat_fit</span> <span class="o">&lt;-</span> <span class="nf">fit</span><span class="p">(</span><span class="n">cat_wf</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">orbital_obj</span> <span class="o">&lt;-</span> <span class="nf">orbital</span><span class="p">(</span><span class="n">cat_fit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">orbital_obj</span></span></span></code></pre></div></div>
<pre><code>── orbital Object ──────────────────────────────────────────────────────────────
• .pred = dplyr::case_when(cyl &lt;= 5 ~ dplyr::case_when(wt &lt;= 2.17 ~ dplyr ...
────────────────────────────────────────────────────────────────────────────────
1 equations in total.
</code></pre>
<p>From here you can predict in-database with <code>predict()</code> against a database connection,
or generate the SQL directly with <code>orbital_sql()</code>.
See the <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital documentation</a> for the full set of supported backends.</p>
<h2 id="wrapping-up">Wrapping up
</h2>
<p>CatBoost is a great addition to the gradient boosting options available in tidymodels,
especially if you work with categorical features or want a strong out-of-the-box model.</p>
<p>For the full details, see the <a href="https://bonsai.tidymodels.org/news/index.html" target="_blank" rel="noopener">bonsai changelog</a> and the <a href="https://parsnip.tidymodels.org/news/index.html#parsnip-140" target="_blank" rel="noopener">parsnip 1.4.0 release notes</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-06-25_catboost-tidymodels/featured.png" length="296364" type="image/png" />
    </item>
    <item>
      <title>Great Tables `v0.22.0`: footnotes, summary rows, column merges, and more</title>
      <link>https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/</link>
      <pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/</guid>
      <dc:creator>Rich Iannone</dc:creator><description><![CDATA[<p><a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables</a> gives you a
grammar for assembling presentation-quality display tables in Python.
You start from a DataFrame, declare which parts of the table mean what
(the stub, the row groups, the column labels), and then layer on
formatting, styling, and annotation until the table communicates exactly
what you intend. The library has always taken much of its design from
the <a href="https://gt.rstudio.com" target="_blank" rel="noopener"><strong>gt</strong> R package</a>, and over successive
releases the Python version has been steadily catching up to the
capabilities that R users have enjoyed for years.</p>
<p>The <code>v0.22.0</code> release is the largest step in that direction so far. It
introduces footnotes, group-wise summary rows, a family of
column-merging methods, a suite of text transformations, several
value-substitution helpers, two new formatting methods, and a modern
image-export pipeline through <code>gtsave()</code>. The LaTeX output gained the
ability to render stubs and row groups, and Pandas is no longer a
required dependency. There is a great deal to cover, so this post walks
through each addition in turn, with a small working example for every
one.</p>
<h2 id="footnotes-with-tab_footnote">Footnotes with <code>tab_footnote()</code>
</h2>
<p>Footnotes are one of the oldest conventions in tabular presentation, and
they solve a real problem: sometimes a value, a label, or a heading
needs a short explanation that would clutter the table if placed inline.
The new <code>tab_footnote()</code> method attaches a footnote to any location in
the table and manages the marks for you, numbering them sequentially in
the order they appear and collecting the notes themselves in the table&rsquo;s
footer.</p>
<p>A location is specified with one of the <code>loc.*</code> helpers, the same ones
used elsewhere in the library for styling. You can attach a note to
cells in the stub, to a column label, to the subtitle, or to body cells.
Because the footnote text accepts <code>md()</code> and <code>html()</code>, you can format it
with Markdown or raw HTML just as you would any other piece of table
content.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="n">md</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">towny</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">towny_mini</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">from_pandas</span><span class="p">(</span><span class="n">towny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</span><span class="s2">&#34;csd_type&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;city&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span><span class="p">([</span><span class="s2">&#34;name&#34;</span><span class="p">,</span> <span class="s2">&#34;density_2021&#34;</span><span class="p">,</span> <span class="s2">&#34;population_2021&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">top_k</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">by</span><span class="o">=</span><span class="s2">&#34;population_2021&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="s2">&#34;population_2021&#34;</span><span class="p">,</span> <span class="n">descending</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">towny_mini</span><span class="p">,</span> <span class="n">rowname_col</span><span class="o">=</span><span class="s2">&#34;name&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_header</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="o">=</span><span class="n">md</span><span class="p">(</span><span class="s2">&#34;The 10 Largest Municipalities in `towny`&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">subtitle</span><span class="o">=</span><span class="s2">&#34;Population values taken from the 2021 census.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_integer</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_label</span><span class="p">(</span><span class="n">density_2021</span><span class="o">=</span><span class="s2">&#34;Density&#34;</span><span class="p">,</span> <span class="n">population_2021</span><span class="o">=</span><span class="s2">&#34;Population&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_footnote</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">footnote</span><span class="o">=</span><span class="s2">&#34;Part of the Greater Toronto Area.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">stub</span><span class="p">(</span><span class="n">rows</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Toronto&#34;</span><span class="p">,</span> <span class="s2">&#34;Mississauga&#34;</span><span class="p">,</span> <span class="s2">&#34;Brampton&#34;</span><span class="p">,</span> <span class="s2">&#34;Markham&#34;</span><span class="p">,</span> <span class="s2">&#34;Vaughan&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">]),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_footnote</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">footnote</span><span class="o">=</span><span class="n">md</span><span class="p">(</span><span class="s2">&#34;Density is in terms of persons per {{km^2}}.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">column_labels</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;density_2021&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_footnote</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">footnote</span><span class="o">=</span><span class="s2">&#34;Census results made public on February 9, 2022.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">subtitle</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_footnote_marks</span><span class="p">(</span><span class="n">marks</span><span class="o">=</span><span class="s2">&#34;letters&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/footnotes.png" alt="A towny table with three lettered footnotes attached to the subtitle, a column label, and several stub cells"></p>
<p>The marks themselves are configurable through <code>opt_footnote_marks()</code>.
The default is a standard set of typographic symbols, but you can switch
to numbers or letters, as we did above with <code>marks=&quot;letters&quot;</code>. The
<code>placement=</code> argument on <code>tab_footnote()</code> controls whether a mark sits
to the left or right of the cell content, and the default <code>&quot;auto&quot;</code>
chooses a side based on the cell&rsquo;s alignment.</p>
<h2 id="group-wise-summaries-with-summary_rows">Group-wise summaries with <code>summary_rows()</code>
</h2>
<p>When a table is divided into row groups, readers frequently want a
per-group summary: a total, a mean, a minimum and maximum. The
<code>summary_rows()</code> method computes these and inserts them as labeled rows
within each group, either at the bottom (the default) or at the top.</p>
<p>The aggregations are described with the <code>fns=</code> argument, a dictionary
whose keys become the row labels and whose values are the expressions to
evaluate. The expressions can be Polars expressions, which is the most
concise option when your data is a Polars DataFrame, or plain Python
callables that receive a DataFrame subset. A formatting function from
the <code>vals.*</code> family can be passed through <code>fmt=</code> so that the summary
values match the formatting of the rest of the table.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">vals</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">gtcars</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gtcars_mini</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">from_pandas</span><span class="p">(</span><span class="n">gtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span><span class="p">([</span><span class="s2">&#34;mfr&#34;</span><span class="p">,</span> <span class="s2">&#34;model&#34;</span><span class="p">,</span> <span class="s2">&#34;hp&#34;</span><span class="p">,</span> <span class="s2">&#34;trq&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">gtcars_mini</span><span class="p">,</span> <span class="n">rowname_col</span><span class="o">=</span><span class="s2">&#34;model&#34;</span><span class="p">,</span> <span class="n">groupname_col</span><span class="o">=</span><span class="s2">&#34;mfr&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">summary_rows</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">fns</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Min&#34;</span><span class="p">:</span> <span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</span><span class="s2">&#34;hp&#34;</span><span class="p">,</span> <span class="s2">&#34;trq&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">min</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Max&#34;</span><span class="p">:</span> <span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</span><span class="s2">&#34;hp&#34;</span><span class="p">,</span> <span class="s2">&#34;trq&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">max</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">fmt</span><span class="o">=</span><span class="n">vals</span><span class="o">.</span><span class="n">fmt_integer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/summary-rows.png" alt="A gtcars table grouped by manufacturer, with Min and Max summary rows beneath each group"></p>
<p>By default the summary applies to every group, but the <code>groups=</code>
argument narrows it to a named subset when you only need summaries in
certain places. The release also includes <code>grand_summary_rows()</code>, a
companion method that produces a single summary across the entire table
rather than one per group. Both kinds of summary can be targeted for
styling through <code>loc.summary()</code> and <code>loc.grand_summary()</code>, so you can
shade them or set them apart from the regular body rows.</p>
<h2 id="merging-columns-together">Merging columns together
</h2>
<p>Tables often hold several columns that, conceptually, describe a single
quantity. A value and its uncertainty, the lower and upper ends of a
range, or a count paired with its percentage all read better as one
column than as two. The release adds a family of merge methods for
exactly these situations, along with a generic method for everything
else.</p>
<p>The most specialized of these is <code>cols_merge_uncert()</code>, which combines a
measured value with its uncertainty and renders the pair with a
plus-or-minus separator. You provide the value column and the
uncertainty column, and the second column is hidden automatically once
it has been folded into the first.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">exibble</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">exibble_mini</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">from_pandas</span><span class="p">(</span><span class="n">exibble</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="s2">&#34;num&#34;</span><span class="p">,</span> <span class="s2">&#34;currency&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">exibble_mini</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;num&#34;</span><span class="p">,</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">use_seps</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_merge_uncert</span><span class="p">(</span><span class="n">col_val</span><span class="o">=</span><span class="s2">&#34;currency&#34;</span><span class="p">,</span> <span class="n">col_uncert</span><span class="o">=</span><span class="s2">&#34;num&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_label</span><span class="p">(</span><span class="n">currency</span><span class="o">=</span><span class="s2">&#34;value + uncert.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/cols-merge-uncert.png" alt="A table merging a value column and an uncertainty column into a single value-plus-uncertainty column"></p>
<p>The <code>cols_merge_range()</code> method works the same way for a pair of columns
that mark the beginning and end of a range, joining them with an en dash
by default (the separator is adjustable through <code>sep=</code>). The
<code>cols_merge_n_pct()</code> method pairs a count with a percentage, rendering
values in the familiar <code>10 (16.70%)</code> form and suppressing the percentage
when the count is zero.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;category&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;A&#34;</span><span class="p">,</span> <span class="s2">&#34;B&#34;</span><span class="p">,</span> <span class="s2">&#34;C&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;n&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">30</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;pct&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mf">0.167</span><span class="p">,</span> <span class="mf">0.333</span><span class="p">,</span> <span class="mf">0.500</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_percent</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;pct&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_merge_n_pct</span><span class="p">(</span><span class="n">col_n</span><span class="o">=</span><span class="s2">&#34;n&#34;</span><span class="p">,</span> <span class="n">col_pct</span><span class="o">=</span><span class="s2">&#34;pct&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_label</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="s2">&#34;Count (%)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/cols-merge-n-pct.png" alt="A table merging a count column and a percentage column into a single Count (%) column"></p>
<p>For anything that does not fit those three patterns there is the generic
<code>cols_merge()</code>, which takes a list of columns and a <code>pattern=</code> template.
The template uses zero-based indices in braces to refer to the columns,
so a pattern of <code>&quot;{0} to {1}&quot;</code> interleaves the first and second columns
with the literal text between them. The first column named becomes the
visible, merged column, and the rest are hidden by default. This is the
general mechanism on which the specialized methods are built, and it is
the right tool when your desired arrangement is unusual.</p>
<p>A related convenience is <code>cols_reorder()</code>, which rearranges every column
in a single call. Previously, a full reordering meant a sequence of
<code>cols_move_*()</code> invocations; now you can list the columns in the order
you want and have the table laid out accordingly. The method expects
every column to appear exactly once, raising an error if any are omitted
or duplicated, which guards against the silent loss of a column.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">exibble</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">exibble_mini</span> <span class="o">=</span> <span class="n">exibble</span><span class="p">[[</span><span class="s2">&#34;num&#34;</span><span class="p">,</span> <span class="s2">&#34;char&#34;</span><span class="p">,</span> <span class="s2">&#34;fctr&#34;</span><span class="p">,</span> <span class="s2">&#34;date&#34;</span><span class="p">,</span> <span class="s2">&#34;time&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">exibble_mini</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_reorder</span><span class="p">([</span><span class="s2">&#34;fctr&#34;</span><span class="p">,</span> <span class="s2">&#34;date&#34;</span><span class="p">,</span> <span class="s2">&#34;time&#34;</span><span class="p">,</span> <span class="s2">&#34;char&#34;</span><span class="p">,</span> <span class="s2">&#34;num&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/cols-reorder.png" alt="An exibble table with its columns rearranged into a custom order"></p>
<h2 id="a-suite-of-text-transformations">A suite of text transformations
</h2>
<p>Formatting methods handle numbers, dates, and currencies, but cell
content sometimes needs a transformation that no formatter anticipates.
The release introduces four <code>text_*()</code> methods that operate on the
rendered text of cells, each addressing a different shape of problem.</p>
<p>The most general is <code>text_transform()</code>, which applies an arbitrary
function to the text of the targeted cells. The function receives the
cell&rsquo;s current string and returns a new one, which makes it suitable for
any transformation you can express in Python. Because it runs after
formatting, you can format a value first and then decorate the result.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="n">exibble</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">exibble</span><span class="p">[[</span><span class="s2">&#34;num&#34;</span><span class="p">,</span> <span class="s2">&#34;char&#34;</span><span class="p">]]</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;num&#34;</span><span class="p">,</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">text_transform</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="p">[</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;num&#34;</span><span class="p">),</span> <span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;char&#34;</span><span class="p">)],</span>
</span></span><span class="line"><span class="cl">        <span class="n">fn</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;~ </span><span class="si">{</span><span class="n">x</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/text-transform.png" alt="An exibble table with a tilde prefix applied to the num and char cells"></p>
<p>When the transformation is a regular-expression substitution,
<code>text_replace()</code> is more direct. It takes a <code>pattern=</code> and a
<code>replacement=</code>, and it supports capture groups, so you can wrap or
rearrange matched text. The example below finds parenthetical text and
emphasizes it with HTML tags.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">loc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;item&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Column A (details)&#34;</span><span class="p">,</span> <span class="s2">&#34;Column B (info)&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;value&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">text_replace</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">pattern</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;\((.+?)\)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">replacement</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;(&lt;em&gt;\1&lt;/em&gt;)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;item&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/text-replace.png" alt="A table with parenthetical text emphasized in the item column"></p>
<p>The remaining two methods cover conditional replacement.
<code>text_case_match()</code> is a switch-like construct: each case is a tuple
pairing one or more values to match against a replacement string, with
an optional <code>default=</code> for everything unmatched. <code>text_case_when()</code>
generalizes this to predicates, where each case pairs a function that
returns a boolean with the replacement to use when it is true. The case
ordering matters, since the first matching predicate wins, which makes
it a natural fit for binning a numeric column into labels.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">loc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">95</span><span class="p">,</span> <span class="mi">72</span><span class="p">,</span> <span class="mi">88</span><span class="p">,</span> <span class="mi">61</span><span class="p">,</span> <span class="mi">100</span><span class="p">]})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">text_case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">90</span><span class="p">,</span> <span class="s2">&#34;A&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">80</span><span class="p">,</span> <span class="s2">&#34;B&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">70</span><span class="p">,</span> <span class="s2">&#34;C&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">default</span><span class="o">=</span><span class="s2">&#34;F&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/text-case-when.png" alt="A score column whose numeric values are replaced by letter grades"></p>
<h2 id="substituting-specific-values">Substituting specific values
</h2>
<p>Closely related to text transformation is the act of replacing
particular values for the sake of readability. A column of measurements
might contain values too small to be meaningful, or zeros that would be
better shown as a dash, or missing entries that should read as something
other than a blank. The release adds a family of <code>sub_*()</code> methods for
these cases: <code>sub_missing()</code> for missing values, <code>sub_zero()</code> for zeros,
<code>sub_small_vals()</code> and <code>sub_large_vals()</code> for values beyond a threshold,
and the general <code>sub_values()</code> for replacing any specified value.</p>
<p>The small-value substitution is representative. It replaces values whose
magnitude falls below a <code>threshold=</code> with a chosen pattern, which is
useful when very small numbers carry no real information and only
distract. The <code>sign=</code> argument restricts the substitution to positive or
negative values, so you can treat the two tails of a distribution
differently.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">neg_vals_df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;i&#34;</span><span class="p">:</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;numbers&#34;</span><span class="p">:</span> <span class="p">[</span><span class="o">-</span><span class="mf">0.0001</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.005</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.05</span><span class="p">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">100.0</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">neg_vals_df</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;numbers&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">sub_small_vals</span><span class="p">(</span><span class="n">sign</span><span class="o">=</span><span class="s2">&#34;-&#34;</span><span class="p">,</span> <span class="n">threshold</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">small_pattern</span><span class="o">=</span><span class="s2">&#34;~0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/sub-small-vals.png" alt="A column of small-magnitude negative numbers replaced by an approximate-zero marker"></p>
<p>These methods operate on the underlying values rather than on rendered
text, so they compose cleanly with the formatting methods. You decide
what counts as missing, zero, small, or large, and the table presents
those cases consistently wherever they occur.</p>
<h2 id="two-new-formatters-durations-and-parts-per">Two new formatters: durations and parts-per
</h2>
<p>The formatting family gained two members. The first, <code>fmt_duration()</code>,
renders durations in any of several styles. Numeric inputs are
interpreted according to an <code>input_units=</code> setting (seconds, minutes,
hours, days, or weeks), while Polars <code>Duration</code> columns are detected
automatically. The <code>duration_style=</code> argument selects between a narrow
style such as <code>5d 3h</code>, a wide style such as <code>5 days, 3 hours</code>, a
colon-separated style such as <code>02:15:30</code>, and ISO 8601. The example
below renders race times as zero-padded <code>HH:MM:SS</code>.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Marathon&#34;</span><span class="p">,</span> <span class="s2">&#34;Half Marathon&#34;</span><span class="p">,</span> <span class="s2">&#34;10K&#34;</span><span class="p">,</span> <span class="s2">&#34;Mile&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;winning_time_s&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">7377</span><span class="p">,</span> <span class="mi">3542</span><span class="p">,</span> <span class="mi">1620</span><span class="p">,</span> <span class="mi">233</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_duration</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;winning_time_s&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">input_units</span><span class="o">=</span><span class="s2">&#34;seconds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">duration_style</span><span class="o">=</span><span class="s2">&#34;colon-sep&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">output_units</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;hours&#34;</span><span class="p">,</span> <span class="s2">&#34;minutes&#34;</span><span class="p">,</span> <span class="s2">&#34;seconds&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/fmt-duration.png" alt="Race times formatted as zero-padded hours, minutes, and seconds"></p>
<p>The second formatter, <code>fmt_partsper()</code>, handles parts-per quantities:
per-mille, parts per million, parts per billion, and finer scales still.
The <code>to_units=</code> argument names the target quantity, the values are
scaled to match unless you opt out with <code>scale_values=False</code>, and the
symbol is rendered appropriately for both HTML and LaTeX output. The
example formats gas concentrations as parts per billion by volume.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">concentrations</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;gas&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;CO&#34;</span><span class="p">,</span> <span class="s2">&#34;NO2&#34;</span><span class="p">,</span> <span class="s2">&#34;O3&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;conc&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">35.0</span><span class="p">,</span> <span class="mf">120.0</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">concentrations</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_partsper</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;conc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">to_units</span><span class="o">=</span><span class="s2">&#34;ppb&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">scale_values</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">symbol</span><span class="o">=</span><span class="s2">&#34;ppbV&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/fmt-partsper.png" alt="Gas concentrations formatted as parts per billion by volume"></p>
<h2 id="saving-tables-as-images-with-gtsave">Saving tables as images with <code>gtsave()</code>
</h2>
<p>A display table is often destined for a slide deck, a report, or a
README, and in those settings you need an image rather than live HTML.
The new <code>gtsave()</code> method produces one by rendering the table in a
headless instance of Chrome and capturing it. It writes PNG, JPEG, WebP,
and PDF, choosing the format from the file extension you supply.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">gtcars</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gtcars_mini</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">from_pandas</span><span class="p">(</span><span class="n">gtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span><span class="p">([</span><span class="s2">&#34;mfr&#34;</span><span class="p">,</span> <span class="s2">&#34;model&#34;</span><span class="p">,</span> <span class="s2">&#34;msrp&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">gtcars_mini</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_header</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Some Cars from gtcars&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_currency</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;msrp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">gtsave</span><span class="p">(</span><span class="s2">&#34;my_table.png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p style="text-align: center;"><img src="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/gtsave-example.png" alt="A small gtcars table with a title and currency-formatted MSRP, saved to a PNG with gtsave()"></p>
<p>Several arguments control the capture. The <code>zoom=</code> factor governs the
resolution of raster output, with higher values producing sharper
images, while <code>expand=</code> adds padding around the table and
<code>vwidth=</code>/<code>vheight=</code> set the viewport. The <code>gtsave()</code> method replaces
the older <code>save()</code>, which is now deprecated; existing code will continue
to work for the time being, but new work should use <code>gtsave()</code>.</p>
<h2 id="better-latex-output">Better LaTeX output
</h2>
<p>Great Tables can render to LaTeX as well as HTML, and that path received
substantial attention in this release. LaTeX output now supports the
stub and row groups, including spanning column headers and the
row-group-as-column layout, which means that tables relying on these
structural features are no longer limited to HTML. In addition, Markdown
and HTML content placed in cells, headers, or footnotes is now converted
to its LaTeX equivalent during rendering, so styled text survives the
trip into a LaTeX document rather than appearing as literal markup. For
anyone producing tables destined for a paper or a typeset report, the
LaTeX output is now much closer in capability to the HTML output.</p>
<h2 id="polars-without-pandas">Polars without Pandas
</h2>
<p>Until now, Great Tables required Pandas even if all of your work was in
Polars. As of this release, Pandas is an optional dependency, and the
library is fully functional with Polars alone. For Polars-first projects
and for lightweight environments where every dependency counts, this
removes a sizable transitive install that was not actually needed.
Pandas users are unaffected: a DataFrame from either library works as
input exactly as before, and the choice of backend remains yours.</p>
<h2 id="getting-started">Getting started
</h2>
<p>Great Tables <code>v0.22.0</code> is available now on PyPI, so a
<code>pip install great-tables</code> (or an upgrade of an existing install) brings
everything described here. The <a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">documentation
site</a>, which moved to <a href="https://posit-dev.github.io/great-docs/" target="_blank" rel="noopener">Great
Docs</a> as its generator in this
release, covers each method in detail with runnable examples, and the
<a href="https://posit-dev.github.io/great-tables/get-started/" target="_blank" rel="noopener">User Guide</a> has
been updated to reflect the new features. The <a href="https://github.com/posit-dev/great-tables" target="_blank" rel="noopener">GitHub
repository</a> holds the source,
the full changelog, and the issue tracker. This release also welcomed
several first-time contributors, and if you would like to join them, or
simply have a feature to request or a bug to report, the issue tracker
is the place to start.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-06-25_great-tables-0-22-0/assets/great-tables-v-0-22-0-release.png" length="1882664" type="image/png" />
    </item>
    <item>
      <title>Investing in the Future of Interactive Computing: We&#39;ve Joined the Jupyter Foundation</title>
      <link>https://opensource.posit.co/blog/2026-06-25_posit-joins-jupyter-foundation/</link>
      <pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-06-25_posit-joins-jupyter-foundation/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<p>At Posit, our mission is to empower data scientists and researchers with the best possible tools to explore, visualize, and share their work. For years, our team has relied on the <a href="https://jupyter.org/" target="_blank" rel="noopener">Jupyter</a> ecosystem, with Notebooks, IPython, JupyterLab, and JupyterHub all integrated across our product offerings and Jupyter kernels powering <a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a>&rsquo;s R and Python support. Today, we are proud to announce that we are deepening our commitment to this community by becoming an official <a href="https://jupyterfoundation.org/" target="_blank" rel="noopener">Jupyter Foundation</a> Member!</p>
<p>Since its inception, <a href="https://jupyter.org/" target="_blank" rel="noopener">Project Jupyter</a> has grown into one of the world&rsquo;s most widely used open source ecosystems for interactive computing, powering breakthroughs in research, education, and industry. The Jupyter Foundation, hosted by the <a href="https://training.linuxfoundation.org/" target="_blank" rel="noopener">Linux Foundation</a>, was established to support the long-term sustainability of this work by bringing together organizations committed to investing in the project&rsquo;s future.</p>
<p>By joining as a Foundation Member, we are becoming active stewards of its future. Our membership helps fund the core infrastructure, the release engineering, and the community events that keep Jupyter at the cutting edge of scientific computing. This move reinforces our promise to the open source community: we will not only build on these tools, but we will also build up the people and processes that create them.</p>
<p>Open source is a shared resource, and it requires shared responsibility. We encourage our partners and peers in the industry to join us in supporting the foundations that make our work possible.</p>
<p>To learn more about our commitment to open research and technical innovation, visit our <a href="https://opensource.posit.co/about/posit/" target="_blank" rel="noopener">About Page</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-06-25_posit-joins-jupyter-foundation/featured.png" length="392627" type="image/png" />
    </item>
  </channel>
</rss>
