<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.franceselliott.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.franceselliott.com/" rel="alternate" type="text/html" /><updated>2026-03-24T20:37:59+00:00</updated><id>https://www.franceselliott.com/feed.xml</id><title type="html">Developer experience consultant and technical writer</title><subtitle>Developer experience and docs</subtitle><author><name>Frances Elliott</name></author><entry><title type="html">Building AI doc toolchains with Claude skills</title><link href="https://www.franceselliott.com/2026/03/10/micropost-claude-skills.html" rel="alternate" type="text/html" title="Building AI doc toolchains with Claude skills" /><published>2026-03-10T00:00:00+00:00</published><updated>2026-03-10T00:00:00+00:00</updated><id>https://www.franceselliott.com/2026/03/10/micropost-claude-skills</id><content type="html" xml:base="https://www.franceselliott.com/2026/03/10/micropost-claude-skills.html"><![CDATA[<p>I’ve been building out some powerful AI-assisted docs toolchain infrastructure recently. Some random observations:</p>

<ul>
  <li>Claude’s plugins are amazing! Especially the official <code class="language-plaintext highlighter-rouge">/skill-creator</code> and <code class="language-plaintext highlighter-rouge">/code-simplifier</code> plugins. I need to check out the official <code class="language-plaintext highlighter-rouge">brainstorming</code> skill next.</li>
  <li>You know how the term “task switching” is in our workplace lexicon? I want to add “pace switching” too. It’s the feeling of working with AI on something blazingly fast, and then abruptly slowing down to carefully check and correct its work. It’s like stumbling off a moving walkway sometimes.</li>
  <li>LLM coding agents are SUCH a loss leader right now – like a $200/month subscription could use up to $2,000 in compute. Designing my AI toolchains to minimize token use seems pretty wise, and I’m already finding ways I can steer LLMs in that direction. Like, “hey, agent, go use the <code class="language-plaintext highlighter-rouge">llms.txt</code> to search the docs, and then fetch the MD raw files. Don’t just use our sensible-docs MCP server for docs search!” Then again, I could be wrong, and LLM token use will get so efficient and so cheap that it won’t be a concern.</li>
  <li>The AI docs-authoring toolchain I’ve built out for my client Sensible is pretty extensive at this point – my <code class="language-plaintext highlighter-rouge">.claude</code>  dir is full of docs-writing skills, preferences, style guides, and templates. And the crazy thing is, they really do seem to work well! I’m growing and changing the infrastructure fast – this snapshot will probably be out of date in half a week:</li>
</ul>

<pre><code class="language-txt">.claude/
├── preferences/
│   └── editorial-preferences.md
└── skills/
    ├── create-concept-doc/
    ├── create-docs-example/
    ├── create-new-doc/
    ├── revise-doc-style/
    ├── sensible-changelog/
    ├── sensible-integration-guide-gen.../
    ├── sensible-style-guide-generator.../
    ├── update-docs-from-pr/
    ├── update-existing-doc/
    └── upload-pr-extractor/

style-guide/
├── changelog-style-guide.md
├── concept-topic-template.md
├── config-library-supported-docu.../
└── integration-guide-template.md
</code></pre>

<p>Next up: check out a <a href="https://github.com/agent-ecosystem/skill-validator">skill-validator</a> whose author claims the output of Claude’s <code class="language-plaintext highlighter-rouge">skill-creator</code> often actually fails her validation tool!</p>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[I’ve been building out some powerful AI-assisted docs toolchain infrastructure recently. Some random observations:]]></summary></entry><entry><title type="html">Wow, all my old LLM experiments are now invalid</title><link href="https://www.franceselliott.com/2026/02/19/november-moment.html" rel="alternate" type="text/html" title="Wow, all my old LLM experiments are now invalid" /><published>2026-02-19T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><id>https://www.franceselliott.com/2026/02/19/november-moment</id><content type="html" xml:base="https://www.franceselliott.com/2026/02/19/november-moment.html"><![CDATA[<p>Gosh, I have <em>really</em> experienced the <a href="https://simonwillison.net/2026/Feb/18/the-ai-disruption/">“November moment”</a> (i.e., the release of GPT-5.2 and Opus 4.5 in November 2025 hugely increased AI capabilities). Just a few months ago, I was getting mediocre results with LLM-assisted docs. Now, I have skills and subagents I’ve generated in Claude Code to assist me in writing docs…and it truly feels revolutionary. I’ll write more about it later, but I want to mark this moment for myself in my personal blog to remember later as a watershed.</p>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[Gosh, I have really experienced the “November moment” (i.e., the release of GPT-5.2 and Opus 4.5 in November 2025 hugely increased AI capabilities). Just a few months ago, I was getting mediocre results with LLM-assisted docs. Now, I have skills and subagents I’ve generated in Claude Code to assist me in writing docs…and it truly feels revolutionary. I’ll write more about it later, but I want to mark this moment for myself in my personal blog to remember later as a watershed.]]></summary></entry><entry><title type="html">How I think about developer experience</title><link href="https://www.franceselliott.com/2026/02/10/dx.html" rel="alternate" type="text/html" title="How I think about developer experience" /><published>2026-02-10T00:00:00+00:00</published><updated>2026-02-10T00:00:00+00:00</updated><id>https://www.franceselliott.com/2026/02/10/dx</id><content type="html" xml:base="https://www.franceselliott.com/2026/02/10/dx.html"><![CDATA[<p>Recently someone asked me to describe my lens on developer experience, and I realized I’ve never really articulated it. So here’s my attempt!</p>

<p>I’d say I’m mostly on the tactical side of DX. If somebody’s providing the overall strategy—like “let’s target junior developers for this on-boarding experience”—I’m the person implementing that vision. And since I think in terms of information architecture and detailed systems of information, I’ve come to believe that <strong>developer experience lives in the details</strong>.</p>

<p>I’m not the person who’s going to completely overhaul a domain model for how we think about an SDK. But I <em>am</em> the person who’s going to think through the implications of a new domain model, and make sure it’s self-consistent. I’m where the rubber hits the road-—like, is this actually working or not working?</p>

<p>Domain overhauls don’t come around every day, though, so I think my biggest day-to-day tactic for DX is  the “docs-first” approach. Is that a techcomm industry term? I feel like it must be. Anyway, when a new feature is under discussion, I mock up documentation to see how easy it is to describe. If it’s awkward – like the naming or interface requires a lot of explanation – then I mock up docs for an alternative interface or naming structure or parameterization of the feature, till I hit on something elegant. Then I propose it.</p>

<p>The other thing about docs-first is that it gives me lead time for stopping developers from creating product sprawl or exposing “inside baseball” features to the public before they’ve gotten into production. Getting rid of a public feature is a much harder ask than not exposing it in the first place.</p>

<p>I’ll walk through some examples from my work with Sensible (a document AI platform) to show what this looks like in practice.</p>

<h2 id="example-1">Example 1</h2>

<p>When my client Sensible was in the early days of LLM features, they used an embeddings approach for scoring relevant document chunks to find prompt context. They soon evolved to finding prompt context through per-page document summarization. Then they leveled up again to offer summarization based on logical splits in the document (like section headings) rather than on page boundaries.</p>

<p>The research engineer who was implementing this outline-based summarization created a <code class="language-plaintext highlighter-rouge">document_outline</code> preprocessor that would chunk a document semantically into logical segments. This outline would then influence how a downstream LLM method (<code class="language-plaintext highlighter-rouge">query_group</code>) searched the document.</p>

<p>I helped the engineer name the preprocessor, but as soon as I mocked up docs showing the interaction between this preprocessor and a downstream <code class="language-plaintext highlighter-rouge">query_group</code> LLM method, I realized there was a domain model inconsistency. The <code class="language-plaintext highlighter-rouge">query_group</code> method already had a <code class="language-plaintext highlighter-rouge">search_by_summarization</code> boolean parameter that controlled how the LLM searched the document. This new preprocessor would create a weird side effect—it would change how <code class="language-plaintext highlighter-rouge">query_group</code> searched, but that configuration lived in a totally different object.</p>

<p>We had a meeting to discuss my findings, in which I proposed turning <code class="language-plaintext highlighter-rouge">search_by_summarization</code> from a boolean into an object with explicit options and backward compatibility. With other engineers’ input, we arrived at a new solution:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">OLD:</span><span class="w"> </span><span class="err">Implicit</span><span class="w"> </span><span class="err">side</span><span class="w"> </span><span class="err">effect</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"preprocessors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"document_outline"</span><span class="w"> </span><span class="p">}</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">Magically</span><span class="w"> </span><span class="err">changes</span><span class="w"> </span><span class="err">search</span><span class="w"> </span><span class="err">behavior</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"query_group"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"search_by_summarization"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">Boolean.</span><span class="w"> </span><span class="err">side-effect:</span><span class="w"> </span><span class="err">This</span><span class="w"> </span><span class="err">boolean</span><span class="w"> </span><span class="err">would</span><span class="w"> </span><span class="err">be</span><span class="w"> </span><span class="err">implicitly</span><span class="w"> </span><span class="err">ignored</span><span class="w"> </span><span class="err">if</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">document_outline</span><span class="w"> </span><span class="err">preprocessor</span><span class="w"> </span><span class="err">is</span><span class="w"> </span><span class="err">configured!</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">My</span><span class="w"> </span><span class="err">proposal:</span><span class="w"> </span><span class="err">Explicit</span><span class="w"> </span><span class="err">configuration</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"query_group"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"search_by_summarization"</span><span class="p">:</span><span class="w"> </span><span class="s2">"outline"</span><span class="w"> </span><span class="err">//</span><span class="w">  </span><span class="err">or</span><span class="w"> </span><span class="s2">"page"</span><span class="err">.</span><span class="w"> </span><span class="err">for</span><span class="w"> </span><span class="err">backward</span><span class="w"> </span><span class="err">compatibility</span><span class="p">,</span><span class="w"> </span><span class="s2">"true"</span><span class="w"> </span><span class="err">defaults</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="s2">"page"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="err">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Thankfully, we avoided creating a preprocessor that would have had these invisible side effects on an extraction method when really we should have been configuring the extraction method directly.</p>

<h2 id="example-2">Example 2</h2>

<p>Sensible automates document data processing, and offers dozens of preprocessors for cleaning up messy documents prior to data extraction, like for OCR, ligatures, or line splitting and merging. A software engineer wanted to create a new <code class="language-plaintext highlighter-rouge">remove_pages</code> preprocessor so users could ignore irrelevant pages. But I quickly pointed out that we already had two preprocessors for manipulating page ranges, as well as existing <code class="language-plaintext highlighter-rouge">match</code> parameters on several preprocessors for selecting pages. So I proposed parameterizing the existing <code class="language-plaintext highlighter-rouge">filter_pages</code> preprocessor using a <code class="language-plaintext highlighter-rouge">match</code> or <code class="language-plaintext highlighter-rouge">match_all</code> parameter for removing pages. That way we’d avoid multiplication of similar preprocessors (and all the docs explaining their differing nuances in usage) and stay consistent with existing patterns. The product manager accepted my proposal. By catching this during the documentation phase, we prevented a redundant feature that would have needed ongoing maintenance and created user confusion about which preprocessor to use.</p>

<h2 id="example-3">Example 3</h2>

<p>When I took on my client Optimizely in March 2020, they were pivoting to a developer audience and planning a major SDK overhaul. They had introduced the notion of feature flags on top of their existing A/B testing features, but the relationship between them was unclear. Were they separate things? Was one a subset of the other?</p>

<p>I’d say my primary contribution to the domain overhaul was getting involved in design conversations early on. I noticed that the API team and UI team weren’t quite aligned on how they were presenting the relationship between the two concepts. Both teams were working toward making flags the root concept, with experiments as a type of flag rule, but their terminology was inconsistent. I could raise points like “we need more correspondence between the <code class="language-plaintext highlighter-rouge">sendEvent</code> parameter and the <code class="language-plaintext highlighter-rouge">decisionLogged</code> field” or “why are we prominently exposing <code class="language-plaintext highlighter-rouge">experimentKey</code> in the API when the new UI will be focused almost exclusively on the <code class="language-plaintext highlighter-rouge">flagKey</code>?” By mocking up documentation for this new model and writing actual code examples, I could spot where the API and UI were diverging in ways that would confuse developers.</p>

<p>The effort for me was worth it, because when I went to conduct an information architecture overhaul of their existing feature flag doc set, I could keep the docs structure clean and consistent, and all the little inconsistencies I would have had to otherwise document and explain were already polished away. For an example of a technical guide I created (since edited by other authors) explaining feature flags, see <a href="https://docs.developers.optimizely.com/feature-experimentation/docs/create-feature-flags">Create feature flags</a>. I like to think that the DX that went into those docs is invisible, unless you’d seen the hypothetical messier experience!</p>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[Recently someone asked me to describe my lens on developer experience, and I realized I’ve never really articulated it. So here’s my attempt!]]></summary></entry><entry><title type="html">My impact as a founding docs writer</title><link href="https://www.franceselliott.com/2026/01/13/results.html" rel="alternate" type="text/html" title="My impact as a founding docs writer" /><published>2026-01-13T00:00:00+00:00</published><updated>2026-01-13T00:00:00+00:00</updated><id>https://www.franceselliott.com/2026/01/13/results</id><content type="html" xml:base="https://www.franceselliott.com/2026/01/13/results.html"><![CDATA[<p>I usually keep track of accolades in a private running doc. Despite my aversion toward self-promo, some of these quotes are just too good not to share. These are the ones I re-read just to warm my heart:</p>

<blockquote>
  <ul>
    <li>
      <p>“…Their documentation is best in class and from what I can tell, our engineers enjoyed integrating [the API]…” - public G2 review</p>
    </li>
    <li>
      <p>“We routinely receive compliments on your docs from other founders, prospective engineering hires, regulatory pen-testers, and implementation partners.” - CEO of client</p>
    </li>
    <li>
      <p>“Your docs are in the #3 top sources of high-quality first prospect meetings for us” - sales team of client</p>
    </li>
    <li>
      <p>“On the strength of your docs when we were an early-stage startup, many of our prospects thought we were well established and had been around for years” - CEO of client</p>
    </li>
    <li>
      <p>“the way your documentation was structured-—it was built for developers. Usually when you see a company that gets that, that’s usually a good smell.” - case study pull quote</p>
    </li>
  </ul>
</blockquote>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[I usually keep track of accolades in a private running doc. Despite my aversion toward self-promo, some of these quotes are just too good not to share. These are the ones I re-read just to warm my heart:]]></summary></entry><entry><title type="html">Implementing docs-hinting best practices for LLMs using Claude Code</title><link href="https://www.franceselliott.com/2025/12/10/implementing-llm-best-practices.html" rel="alternate" type="text/html" title="Implementing docs-hinting best practices for LLMs using Claude Code" /><published>2025-12-10T00:00:00+00:00</published><updated>2025-12-10T00:00:00+00:00</updated><id>https://www.franceselliott.com/2025/12/10/implementing-llm-best-practices</id><content type="html" xml:base="https://www.franceselliott.com/2025/12/10/implementing-llm-best-practices.html"><![CDATA[<p>I’m implementing an LLM best practice for a docs site I wrote for my client Sensible using Claude Code. 
Specifically, I generated an <a href="https://llmstxt.org/">llms.txt</a> file at the root of a docs GitHub repo that lists file directory structure and topics descriptions, to help LLMs interpret the docs site.</p>

<p>It was my first time using Claude Code, and witnessing an LLM edit <em>local files</em> on <em>my</em> laptop was mind-blowing. I’m now fired up about other docs-automation coding projects, and I’ve already made my first few commits on a project I’ll probably blog about soon!</p>

<p>To return to llms.txt –  I’d heard that Claude Code could easily go off the rails without proper guidance, and I see it’s true! Even with such a simple task, I had to continually prevent it from barking up the wrong tree. The descriptions it generated in llms.txt needed some light hand editing, too.</p>

<p>I also then used the llms.txt to implement another best practice (adding descriptions to the frontmatter of our MD topics, a long-neglected task). I’ll figure out how to automate updating llms.txt and keep the descriptions in sync using Claude Code and GitHub actions later.</p>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[I’m implementing an LLM best practice for a docs site I wrote for my client Sensible using Claude Code. Specifically, I generated an llms.txt file at the root of a docs GitHub repo that lists file directory structure and topics descriptions, to help LLMs interpret the docs site.]]></summary></entry><entry><title type="html">Guide to recent breaking Readme API changes</title><link href="https://www.franceselliott.com/2025/11/18/readme-upgrade-migration.html" rel="alternate" type="text/html" title="Guide to recent breaking Readme API changes" /><published>2025-11-18T00:00:00+00:00</published><updated>2025-11-18T00:00:00+00:00</updated><id>https://www.franceselliott.com/2025/11/18/readme-upgrade-migration</id><content type="html" xml:base="https://www.franceselliott.com/2025/11/18/readme-upgrade-migration.html"><![CDATA[<p>I just finished upgrading one of my documentation projects to Readme’s newest platform version. The upgrade was mandatory, and before I embarked on it, I had several pressing concerns<sup>1</sup> that I’ve since cleared up through sandbox tests in advance of the migration. Since I didn’t find the answers when I was skimming Readme’s docs, I’ll note these advanced Q&amp;As here in case they help documentarians:</p>

<ul>
  <li><strong>Question</strong>:  I’m worried about obscure and difficult-to-troubleshoot syntax errors when I move from a lax markup (MD) to a strict markup (MDX). In my experience, MDX has lousy error messages. Will this be a terrible PITA requiring line-by-line editing?
    <ul>
      <li><strong>Answer:</strong> I found that LLMs were really helpful for resolving unclear syntax error messages. My most successful strategy was to provide Claude with a complete MD topic as context and prompt, <code class="language-plaintext highlighter-rouge">Which lines in this topic contain MDX syntax errors? Provide reasoning, evidence, and line numbers</code>.  Asking for “reasoning” and “evidence” seemed to greatly improve the results. Of course, it wasn’t perfect – as a sanity test, I asked the LLM if an MD topic contained any curly braces that weren’t properly contained in code fences. The LLM replied no – even though I was staring at the offending curly brace in question!</li>
    </ul>
  </li>
  <li><strong>Question</strong>: What’s the deal with GitHub branching and bidirectional syncing? Do I have to rename my <code class="language-plaintext highlighter-rouge">main</code> branch in GitHub to a semantic version?
    <ul>
      <li><strong>Answer:</strong> I believe that Readme expects you to ignore the GitHub <code class="language-plaintext highlighter-rouge">main</code> branch, and instead treat a versioned branch as your permanently open, published branch, e.g., <code class="language-plaintext highlighter-rouge">v1.0</code>. That seems to be working for me at present, but what that meant is that I was constantly ignoring prompts from GitHub to create pull requests from my versioned branch into <code class="language-plaintext highlighter-rouge">main</code>, until I changed my default branch in my repo settings. My main branch is simply empty at this point; renaming it or adding content to it seems to immediately break the sync.</li>
    </ul>
  </li>
  <li><strong>Question</strong>: Will the conversion from MD to MDX strip any of my markup tags?
    <ul>
      <li><strong>Answer:</strong> I’ve noticed all my formatting tags are respected, whew!  In particular, I noticed that the conversion preserved my <code class="language-plaintext highlighter-rouge">&lt;br/&gt;</code> tags in tables, for example – hugely important for some of my reference topic formatting, and not a given for MD to MDX conversions.</li>
    </ul>
  </li>
  <li><strong>Question:</strong> Can I sync my openapi YAML spec, or is this a one-time import? Maintaining an API reference in MDX would be a deal-breaking inefficiency!
    <ul>
      <li><strong>Answer:</strong> Yes, I can sync it with Readme’s bidirectional sync feature, contrary to my initial impression! If I import my spec file to the webapp, I get a chance to disable webapp modifications. After that, Readme persists a copy of my spec in the source GitHub file directory; I just point my URL in the webapp to that new instance, and the spec is synced.<sup>2</sup></li>
    </ul>
  </li>
  <li><strong>Question:</strong> I see Readme’s latest CLI demands a different file structure than I currently use. Must I manually restructure static file directories and reauthor their metadata frontmatter? That would be a PITA!<sup>3</sup>
    <ul>
      <li><strong>Answer:</strong> If I use bidirectional sync and avoid the CLI, then no, I don’t have to restructure my files. Readme automatically upgrades me and populates the webapp with my new MDX-compliant topics; I point the webapp admin site at a target empty GitHub repo, and it syncs the files to that repo in the desired file format and directory structure. Yay! I can effectively abandon my old MD files; I don’t have to convert them by hand.</li>
    </ul>
  </li>
  <li><strong>Question:</strong> Is GitHub actually syncing? Can I force a sync? Why aren’t my changes showing up?
    <ul>
      <li><strong>Answer:</strong> Bidirectional syncing is a lot slower than my previous integration, on the order of minutes. I haven’t yet found a way to manually kick off a sync, but I notice the flow is much faster from the webapp to GitHub than from GitHub to the webapp. I also notice that a webapp page edit won’t sync to GitHub until I both save AND navigate away from the page (at least if I’m syncing to my published branch). I wish I could see sync status through a GitHub action, but alas, that’s not the case.</li>
    </ul>
  </li>
  <li><strong>Question:</strong> Will my changelogs sync?
    <ul>
      <li><strong>Answer:</strong> Nope. They don’t show up in GitHub; I still have to create them in the webapp.</li>
    </ul>
  </li>
</ul>

<p><strong>Footnotes</strong></p>

<ol>
  <li>
    <p>The biggest was that the mandatory upgrade broke my existing third-party GitHub <a href="https://github.com/flowcommerce/readme-sync">integration</a>, which relied on an older version of the Readme API. I chose it at a time when Readme’s own GitHub integration was much less powerful than it now is. I will move heaven and earth to achieve static site generation rather than submit to the inefficiencies inherent in authoring docs in a webapp! Even if it meant choosing a third-party open source tool that’s now archived, it was still worth it.</p>
  </li>
  <li>
    <p>You can’t use bidirectional sync and the CLI together; they’re separate tools requiring different static file directory structures. The structure required by the bidirectional sync appears more elegant and maintainable to me.</p>
  </li>
  <li>
    <p>It <em>does</em> appear that through the web app, someone can make minor edits to the MDX in a special section of each reference page that doesn’t show up in the spec source. Readme combines those MDX edits plus your spec file to publish the API reference. I’m guessing this afforance is for contributors who want to add general info through the Readme web UI, but don’t want to get into the weeds of API reference editing. I’ll probably migrate any such contributions out of the MDX and into my spec just to keep things centralized.</p>
  </li>
</ol>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[I just finished upgrading one of my documentation projects to Readme’s newest platform version. The upgrade was mandatory, and before I embarked on it, I had several pressing concerns1 that I’ve since cleared up through sandbox tests in advance of the migration. Since I didn’t find the answers when I was skimming Readme’s docs, I’ll note these advanced Q&amp;As here in case they help documentarians:]]></summary></entry><entry><title type="html">Absent proprietary data, AI blog posts are all dancing to the same tune</title><link href="https://www.franceselliott.com/2025/10/06/ai-jazz-hand.html" rel="alternate" type="text/html" title="Absent proprietary data, AI blog posts are all dancing to the same tune" /><published>2025-10-06T00:00:00+00:00</published><updated>2025-10-06T00:00:00+00:00</updated><id>https://www.franceselliott.com/2025/10/06/ai-jazz-hand</id><content type="html" xml:base="https://www.franceselliott.com/2025/10/06/ai-jazz-hand.html"><![CDATA[<p>Blogging isn’t my main focus – documentation, developer experience, and information architecture is. That said, I’m happy to help out my client Sensible with some blog content, since I’m extremely well positioned to do so.</p>

<p>To that end, recently I researched competitors’ blog content for Sensible. I searched on a particular blog post theme title (I googled “Highs and lows of extracting data from bank statements”, if you’re interested). I was surprised by how many of the top search results were obviously (to my eye) AI generated, and how similar they all were. Aside from being uniformly generic and high level, they sometimes contained the <em>same</em> headings and bullet points with minor variations. They were even the same at the level of <em>specific</em> phrases. “In today’s data-driven world…” showed up in the introductions more times than I could count. It strongly reminded me of the conman who sells the same “spirit fingers” choreography to competing cheerleaders in the 2000 movie <a href="https://youtu.be/-ktJ3w0RH90?si=MNK5btl-HDGrOBaN&amp;t=241">Bring It On</a>. Everyone’s been sold the same song and dance!</p>

<p>Anyway, I’m currently preparing to write a new series of AI-assisted blog posts for this client, and as always, I think the way to actually provide value to the reader is to ground the post in data only my client has access to. In my last series, the posts were grounded in the handwritten code samples and the comments the AI generated for those samples. This time around, the series will be grounded in transcribed interviews I conduct with our CSE team, so we can tell real (anonymized) implementation stories, with all their highs and lows.</p>]]></content><author><name>Frances Elliott</name></author><summary type="html"><![CDATA[Blogging isn’t my main focus – documentation, developer experience, and information architecture is. That said, I’m happy to help out my client Sensible with some blog content, since I’m extremely well positioned to do so.]]></summary></entry><entry><title type="html">Mixed results with AI-assisted changelogs</title><link href="https://www.franceselliott.com/2025/06/30/ai-changelog.html" rel="alternate" type="text/html" title="Mixed results with AI-assisted changelogs" /><published>2025-06-30T00:00:00+00:00</published><updated>2025-06-30T00:00:00+00:00</updated><id>https://www.franceselliott.com/2025/06/30/ai-changelog</id><content type="html" xml:base="https://www.franceselliott.com/2025/06/30/ai-changelog.html"><![CDATA[<p>Recently I experimented with prompting Claude to generate new-feature release announcements based on docs and past changelog examples.</p>

<p>I found a pattern consistent with my other experiments: AI-generated changelogs are fine for higher-level, less technical announcements, and fairly unsatisfactory for more technical or detailed announcements.</p>

<p>Here are a few examples.</p>

<h2 id="describing-new-features">Describing new features</h2>

<p>I wanted to announce a new feature for my client <a href="https://sensible.so/">Sensible</a>. End users can now use “named anchors” to define, then reference a text match, for more concise syntax.</p>

<p>Here’s Claude’s attempt, and my beef with it:</p>

<h3 id="claude-output">Claude output</h3>

<blockquote>
  <p>New feature: Named anchors for reusable anchor definitions</p>

  <p>You can now define named anchors and reuse them across multiple fields using the new <code class="language-plaintext highlighter-rouge">anchorDefinition</code> type. This eliminates redundant anchor configuration and enables cleaner syntax when multiple fields share the same anchor logic.
Define a named anchor with <code class="language-plaintext highlighter-rouge">"type": "anchorDefinition"</code> and a <code class="language-plaintext highlighter-rouge">name</code> parameter, then reference it in other fields using <code class="language-plaintext highlighter-rouge">"anchor": {"ref": "anchor_name"}</code>. Named anchors work within Conditional methods, allowing you to define language-specific or context-dependent anchors that can be referenced by subsequent fields.</p>

  <p>For more information, see <a href="https://docs.sensible.so/docs/anchor#reuse-anchor-variables">Reuse anchor variables</a>.</p>
</blockquote>

<p>Commentary: This assumes the reader knows an anchor’s purpose, which isn’t a given. It’s also overly verbose given that most technical readers are familiar with variable declaration and reference as fundamental to coding languages. A code example would be much simpler. It also misses out on an advanced use case.</p>

<h3 id="my-version">My version</h3>

<p>Here’s my stab at it. For the sake of the experiment, I wrote this before prompting Claude so I wouldn’t bias my own writing:</p>

<blockquote>
  <p>You can now name and reuse <a href="doc:anchor">anchors</a> as variables for concise syntax. Anchors are labels that locate data for layout-based methods.</p>

  <p>Example syntax:</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"fields": [
    {
      /* declare a complex named anchor variable for later reuse */
      "type": "anchorDefinition",
      "name": "liability_workers_comp",
      "anchor": {
        "start":{"text":"Certificate of liability insurance","type":"equals"},
        "match":{"text":"workers compensation","type":"equals"},
        "end":{"type":"equals","text":"the acord name and logo are registered marks of acord"}
      }
    },
    {
      /* extract text from a box 1.15" to the right of the named anchor  */
      "id": "subrogation_waived",
      "method": {
        "id": "box",
        "offsetX": 1.15,
        "position": "right"
      },
      "anchor": {
        "ref": "liability_workers_comp"
      }
    },
</code></pre></div>  </div>

  <p>You can <a href="doc:conditional">conditionally</a> assign values to anchor variables depending on previously extracted document data. For example, assign a <code class="language-plaintext highlighter-rouge">"customer_id"</code> anchor the value of either <code class="language-plaintext highlighter-rouge">"customer number"</code> or <code class="language-plaintext highlighter-rouge">"identificación del cliente"</code> depending on the form’s language. For more information, see <a href="doc:anchor-nuances">Anchor variables</a>.</p>
</blockquote>

<h2 id="deprecating-old-features">Deprecating old features</h2>

<p>I was also unsatisfied with Claude’s attempt to write a deprecation announcement. I wanted to communicate a change to my client’s code editor.</p>

<h3 id="claude-output-1">Claude output</h3>

<p>Here’s Claude’s attempt:</p>

<blockquote>
  <h3 id="deprecation-sensible-instruct-visual-authoring-interface-removed">Deprecation: Sensible Instruct visual authoring interface removed</h3>

  <p>The Sensible Instruct visual authoring interface has been removed. Configuration authoring is now exclusively done in JSON using SenseML syntax.</p>

  <p>The updated interface provides a streamlined experience with compact visual output representation and JSON output in the right pane. You can toggle between these views while working entirely in the JSON-based SenseML editor.</p>

  <p>Previously supported Sensible Instruct methods (Query, List, and Table) remain available as SenseML methods with JSON configuration.</p>
</blockquote>

<p>Commentary: Again, lots of telling without showing. Too much passive voice. Without some tailor-made prompting, Claude also can’t glean from the docs that we’re removing this feature in order to pave the way for a better feature.</p>

<h3 id="my-version-1">My version</h3>

<p>Here’s my version, again written without glancing at Claude:</p>

<blockquote>
  <h2 id="deprecation-sensible-instruct-visual-authoring-tool">Deprecation: Sensible Instruct visual authoring tool</h2>

  <p>We’ve removed <a href="doc:https://docs.sensible.so/changelog/april-2023#visual-authoring-with-gpt-4-sensible-instruct">Sensible Instruct</a>, a visual tool for authoring LLM-based SenseML methods that augmented JSON-based authoring. Stay tuned for new ways to author LLM-based methods in future.</p>

  <p>By default, you now author all SenseML methods in JSON and view extracted document data in a compact visual format. You can also switch between viewing output visually or as JSON.</p>

  <p>Compact visual view of output:
[screenshot]</p>

  <p>JSON view of output:
[screenshot]</p>
</blockquote>]]></content><author><name>frances</name></author><summary type="html"><![CDATA[Recently I experimented with prompting Claude to generate new-feature release announcements based on docs and past changelog examples.]]></summary></entry><entry><title type="html">Video walkthrough - AI-assisted developer advocate blogging</title><link href="https://www.franceselliott.com/2025/05/20/ai-assisted-developer-advocate-blogging.html" rel="alternate" type="text/html" title="Video walkthrough - AI-assisted developer advocate blogging" /><published>2025-05-20T08:00:00+00:00</published><updated>2025-05-20T08:00:00+00:00</updated><id>https://www.franceselliott.com/2025/05/20/ai-assisted-developer-advocate-blogging</id><content type="html" xml:base="https://www.franceselliott.com/2025/05/20/ai-assisted-developer-advocate-blogging.html"><![CDATA[<p>Here’s a short video tour of how I’ve configured Claude with style guides and a knowledge base to automate some of my more formulaic developer advocate tutorials for my client <a href="https://www.sensible.so/">Sensible</a>. It’s quite off-the-cuff and informal, so don’t go looking for production polish in this one!</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/5I0nG5c2Bc0?vq=720" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>

<!--<iframe width="560" height="315" src="https://www.youtube.com/embed/X0vXnEuP-aU?si=oA7oas3Tzw-e0ZOG&hd=1" title="Quick walkthrough Claude project" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>-->

<p>Here’s a recent example of a blog post published using this setup:</p>

<p><a href="https://www.sensible.so/blog/how-to-extract-data-from-cms-1500-forms-with-sensible">How to extract data from CMS 1500 forms with Sensible</a></p>

<p>And for comparison’s sake, here’s an example of a similar blog post I wrote without AI assistance:</p>

<p><a href="https://www.sensible.so/blog/how-to-extract-data-from-rent-rolls-with-llms-and-sensible">How to extract from rent rolls with LLMs and Sensible</a></p>

<p>I should note that even with a fairly successful prompt like this one, there’s still a substantial editorial process. For example, for this blog post, I made the following manual edits:</p>

<ul>
  <li>Corrected some inaccurately generated inline code comments (code hallucination is still a thing even with a prebuilt example config!)</li>
  <li>Added lots of inline comments to the code examples</li>
  <li>Corrected a misstatement about the purpose of a feature (‘fingerprints are for classifying document types!’ Well, no, they’re for classifying document <em>subtypes</em>)</li>
  <li>Inserted a bunch of screenshots</li>
  <li>Added in a bunch of prerequisite setup steps that Claude missed, even though those steps were in the prompt context (sign up for an account; clone cms-1500 support to your account)</li>
  <li>Slimmed down verbose fluff and nixed some marketing hyperbole (‘this is the perfect method for…’)</li>
  <li>Modified the example PDF to provide a better example for a complex feature (the sections method)</li>
  <li>Rewrote a heavily hallucinated conclusion (‘signup for a free account for $100/month!’)</li>
</ul>

<p>Even with this editorial process, Claude still roughly halved the time I’d normally spend on a post like this. I expect it to save me even more time in the future, since I spent a lot of that time on reusable Claude project configuration.</p>

<!-- test loom
<iframe width="560" height="315" src="https://www.youtube.com/embed/Z-RBcErFeoc?si=HkXuGbPzBvZGkVVS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> -->

<!-- higher quality smaller screen OBS

<iframe width="560" height="315" src="https://www.youtube.com/embed/VysEwKkgw20?si=_OISSAGDb-rRyl4Z" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> -->]]></content><author><name>frances</name></author><summary type="html"><![CDATA[Here’s a short video tour of how I’ve configured Claude with style guides and a knowledge base to automate some of my more formulaic developer advocate tutorials for my client Sensible. It’s quite off-the-cuff and informal, so don’t go looking for production polish in this one!]]></summary></entry><entry><title type="html">AI-assisted docs reference writing for a niche scripting language</title><link href="https://www.franceselliott.com/2025/05/08/ai-assisted-docs.html" rel="alternate" type="text/html" title="AI-assisted docs reference writing for a niche scripting language" /><published>2025-05-08T17:55:00+00:00</published><updated>2025-05-08T17:55:00+00:00</updated><id>https://www.franceselliott.com/2025/05/08/ai-assisted-docs</id><content type="html" xml:base="https://www.franceselliott.com/2025/05/08/ai-assisted-docs.html"><![CDATA[<p>Here are the results of my experiments with prompting an AI to write reference docs for a custom extension to a rather niche, obscure scripting language (JsonLogic).</p>

<p>TLDR: The AI performed poorly. It hallucinated heavily about the niche language, to the extent that its code examples were worse than useless. This is in contrast to the success I’ve had with basic code examples in well-known languages like Python.</p>

<p>My methodology was as follows:</p>

<ol>
  <li>
    <p>I authored the reference docs for the custom operator myself, using the unit tests and comments in the operator’s pull request.</p>
  </li>
  <li>
    <p>I asked Claude to author the reference docs. As a baseline, I gave it the code declaring the operator’s functionality. Beyond that, I provided varying levels of context:</p>
    <ol>
      <li>I prompted with zero additional context</li>
      <li>I prompted with my documentation as context  (in Claude’s project knowledge)</li>
      <li>I prompted with the context of Claude’s project knowledge + specific examples of my reference documentation for other JsonLogic custom operators.</li>
    </ol>
  </li>
</ol>

<p>Prompt #3 was the most successful, but none of these prompts returned satisfactory results. For comparison, here’s my docs, versus the results of the most successful prompt.</p>

<h2 id="the-handcrafted-docs">The handcrafted docs</h2>

<blockquote>
  <h2 id="let">Let</h2>

  <p>Use this operator to declare named variables scoped to the Let operator. This operator addresses JsonLogic’s lack of built-in support for named variable declaration.</p>

  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">  
  </span><span class="nl">"let"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
    </span><span class="p">{</span><span class="w"> </span><span class="err">/*</span><span class="w"> </span><span class="mi">1</span><span class="err">st</span><span class="w"> </span><span class="err">arg:</span><span class="w"> </span><span class="err">initialize</span><span class="w"> </span><span class="err">named</span><span class="w"> </span><span class="err">variables</span><span class="w"> </span><span class="err">using</span><span class="w"> </span><span class="err">key/value</span><span class="w"> </span><span class="err">pair</span><span class="w"> </span><span class="err">syntax</span><span class="w"> </span><span class="err">*/</span><span class="w"> </span><span class="p">},</span><span class="w">  
    </span><span class="p">{</span><span class="w"> </span><span class="err">/*</span><span class="w"> </span><span class="mi">2</span><span class="err">nd</span><span class="w"> </span><span class="err">arg:</span><span class="w"> </span><span class="err">operate</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">named</span><span class="w"> </span><span class="err">variables.</span><span class="w"> </span><span class="err">Sensible</span><span class="w"> </span><span class="err">evaluates</span><span class="w"> </span><span class="err">variables</span><span class="w"> </span><span class="err">in</span><span class="w"> </span><span class="err">order</span><span class="w"> </span><span class="err">*/</span><span class="w"> </span><span class="p">}</span><span class="w">  
  </span><span class="p">]</span><span class="w">  
</span><span class="p">}</span><span class="w">  
</span></code></pre></div>  </div>

  <h3 id="example">Example</h3>

  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">  
  </span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">  
  </span><span class="nl">"postprocessor"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jsonLogic"</span><span class="p">,</span><span class="w">  
    </span><span class="nl">"rule"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
      </span><span class="nl">"let"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
        </span><span class="p">{</span><span class="w">  
          </span><span class="err">/*</span><span class="w"> </span><span class="err">declare</span><span class="w"> </span><span class="err">values</span><span class="w"> </span><span class="err">with</span><span class="w"> </span><span class="err">key:value</span><span class="w"> </span><span class="err">pair</span><span class="w"> </span><span class="err">syntax</span><span class="w"> </span><span class="err">*/</span><span class="w">  
          </span><span class="nl">"a"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">  
          </span><span class="nl">"b"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">  
          </span><span class="err">/*</span><span class="w"> </span><span class="err">c</span><span class="w"> </span><span class="err">is</span><span class="w"> </span><span class="err">sum</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="err">a</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="err">b</span><span class="w"> </span><span class="err">(</span><span class="mi">8</span><span class="err">)</span><span class="w"> </span><span class="err">*/</span><span class="w">  
          </span><span class="nl">"c"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
            </span><span class="nl">"+"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
              </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a"</span><span class="w"> </span><span class="p">},</span><span class="w">  
              </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"b"</span><span class="w"> </span><span class="p">}</span><span class="w">  
            </span><span class="p">]</span><span class="w">  
          </span><span class="p">}</span><span class="w">  
        </span><span class="p">},</span><span class="w">  
        </span><span class="err">/*</span><span class="w"> </span><span class="err">operate</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">named</span><span class="w"> </span><span class="err">values</span><span class="w"> </span><span class="err">(</span><span class="mi">5</span><span class="w"> </span><span class="err">*</span><span class="w"> </span><span class="mi">8</span><span class="err">)</span><span class="w"> </span><span class="err">*/</span><span class="w">  
        </span><span class="p">{</span><span class="w">  
          </span><span class="nl">"*"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
            </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a"</span><span class="w"> </span><span class="p">},</span><span class="w">  
            </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c"</span><span class="w"> </span><span class="p">}</span><span class="w">  
          </span><span class="p">]</span><span class="w">  
        </span><span class="p">}</span><span class="w">  
      </span><span class="p">]</span><span class="w">  
    </span><span class="p">}</span><span class="w">  
  </span><span class="p">}</span><span class="w">  
</span><span class="p">}</span><span class="w">  
</span></code></pre></div>  </div>

  <p>This returns:</p>

  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">/*</span><span class="w"> </span><span class="err">postprocessor</span><span class="w"> </span><span class="err">output</span><span class="w"> </span><span class="err">*/</span><span class="w">  
</span><span class="mi">40</span><span class="w">  
</span></code></pre></div>  </div>
</blockquote>

<h2 id="the-ai-generated-docs">The AI-generated docs</h2>

<h3 id="prompt">Prompt</h3>

<p>Here’s the prompt:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>write a reference documentation page for this custom JsonLogic operation,
in the same style as the documentation pages in your project knowledge base:

function op_let(
  // content redacted; private code base
logicEngine.addMethod(
  "let",
  { traverse: false, method: op_let },
  { useContext: true }
);
</code></pre></div></div>

<p>The results:</p>

<ul>
  <li>Were far too verbose</li>
  <li>Failed to explain the ‘why’ for this custom operation, which is simply that JsonLogic lacks native support for variable declaration.</li>
  <li>Failed to explain the scope for the variable declaration, a key point.</li>
  <li>Were very prone to code sample hallucination. The first few prompts, for example, resulted in a code sample that made heavy use of a <code class="language-plaintext highlighter-rouge">do</code> operation that’s not supported in JsonLogic.</li>
  <li>Failed to provide a concise, useful description of the method’s args</li>
</ul>

<p>Here are the results:</p>

<blockquote>
  <h2 id="let-operation">Let operation</h2>
  <p>Use the let operation to define temporary variables within the scope of a JsonLogic expression. This provides a way to create named intermediate values that can be referenced in subsequent expressions, making complex transformations more readable and maintainable.</p>

  <h3 id="parameters">Parameters</h3>
  <p><strong>Parameter</strong> | <strong>Description</strong><br />
— | —<br />
1 | An object with variable names as keys and JsonLogic expressions as values<br />
2 | The JsonLogic expression to evaluate using the defined variables</p>

  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">  
  </span><span class="nl">"let"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
    </span><span class="p">{</span><span class="w">  
      </span><span class="nl">"variable1"</span><span class="p">:</span><span class="w"> </span><span class="err">JsonLogic</span><span class="w"> </span><span class="err">expression</span><span class="w"> </span><span class="err">for</span><span class="w"> </span><span class="err">value</span><span class="mi">1</span><span class="p">,</span><span class="w">  
      </span><span class="nl">"variable2"</span><span class="p">:</span><span class="w"> </span><span class="err">JsonLogic</span><span class="w"> </span><span class="err">expression</span><span class="w"> </span><span class="err">for</span><span class="w"> </span><span class="err">value</span><span class="mi">2</span><span class="p">,</span><span class="w">  
      </span><span class="err">...</span><span class="w">  
    </span><span class="p">},</span><span class="w">  
    </span><span class="err">JsonLogic</span><span class="w"> </span><span class="err">expression</span><span class="w"> </span><span class="err">using</span><span class="w"> </span><span class="err">defined</span><span class="w"> </span><span class="err">variables</span><span class="w">  
  </span><span class="p">]</span><span class="w">  
</span><span class="p">}</span><span class="w">  
</span></code></pre></div>  </div>

  <h3 id="examples">Examples</h3>
  <p><strong>Config</strong></p>

  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">  
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"calculated_invoice"</span><span class="p">,</span><span class="w">  
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customComputation"</span><span class="p">,</span><span class="w">  
    </span><span class="nl">"jsonLogic"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
      </span><span class="nl">"let"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
        </span><span class="p">{</span><span class="w">  
          </span><span class="nl">"subtotal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"invoice_subtotal.value"</span><span class="w"> </span><span class="p">},</span><span class="w">  
          </span><span class="nl">"tax_rate"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tax_rate.value"</span><span class="w"> </span><span class="p">},</span><span class="w">  
          </span><span class="nl">"tax_amount"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"*"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">  
            </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"subtotal"</span><span class="w"> </span><span class="p">},</span><span class="w">  
            </span><span class="p">{</span><span class="w"> </span><span class="nl">"/"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tax_rate"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w">  
          </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w">  
        </span><span class="p">},</span><span class="w">  
        </span><span class="p">{</span><span class="w">  
          </span><span class="nl">"eachKey"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
            </span><span class="nl">"invoice_summary"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
              </span><span class="nl">"eachKey"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
                </span><span class="nl">"subtotal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"subtotal"</span><span class="w"> </span><span class="p">},</span><span class="w">  
                </span><span class="nl">"calculated_tax"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tax_amount"</span><span class="w"> </span><span class="p">},</span><span class="w">  
                </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"+"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"subtotal"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tax_amount"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">},</span><span class="w">  
                </span><span class="nl">"tax_rate_percentage"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"cat"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"var"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tax_rate"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s2">"%"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w">  
              </span><span class="p">}</span><span class="w">  
            </span><span class="p">}</span><span class="w">  
          </span><span class="p">}</span><span class="w">  
        </span><span class="p">}</span><span class="w">  
      </span><span class="p">]</span><span class="w">  
    </span><span class="p">}</span><span class="w">  
  </span><span class="p">}</span><span class="w">  
</span><span class="p">}</span><span class="w">  
</span></code></pre></div>  </div>

  <p><strong>Example document</strong><br />
The following image shows the example document used with this example config:<br />
<em>Show Image</em></p>

  <p><strong>Output</strong></p>
  <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">  
  </span><span class="nl">"calculated_invoice"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
    </span><span class="nl">"invoice_summary"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">  
      </span><span class="nl">"subtotal"</span><span class="p">:</span><span class="w"> </span><span class="mf">850.00</span><span class="p">,</span><span class="w">  
      </span><span class="nl">"calculated_tax"</span><span class="p">:</span><span class="w"> </span><span class="mf">68.00</span><span class="p">,</span><span class="w">  
      </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mf">918.00</span><span class="p">,</span><span class="w">  
      </span><span class="nl">"tax_rate_percentage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8%"</span><span class="w">  
    </span><span class="p">},</span><span class="w">  
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="w">  
  </span><span class="p">}</span><span class="w">  
</span><span class="p">}</span><span class="w">  
</span></code></pre></div>  </div>

  <h3 id="notes">Notes</h3>
  <ul>
    <li>Variables defined in the let operation are accessible only within the scope of the second parameter.</li>
    <li>Variable definitions can reference previously defined variables within the same let operation. For example, if you define variables x and y, the definition of y can reference x.</li>
    <li>Variables are evaluated in the order they appear in the definition object.</li>
    <li>The operation maintains access to the document context, so you can still use the var operator to access document data.</li>
    <li>Variable names in the let operation can be any valid JSON property name.</li>
    <li>If a variable name conflicts with a field in the document context, the variable definition takes precedence within the scope of the let operation.</li>
  </ul>

  <h3 id="use-cases">Use cases</h3>
  <p>This operation is particularly useful for:</p>
  <ul>
    <li>Breaking down complex calculations into named steps for better readability</li>
    <li>Avoiding repeating the same calculation multiple times</li>
    <li>Creating intermediate values that can be referenced multiple times in a complex transformation</li>
    <li>Building complex objects with multiple derived fields</li>
    <li>Organizing multi-step data transformations in a clear, logical manner</li>
  </ul>

  <p>For other JsonLogic operations in Sensible, see JsonLogic extensions.</p>
</blockquote>]]></content><author><name>frances</name></author><category term="techcomm" /><category term="dita" /><summary type="html"><![CDATA[Here are the results of my experiments with prompting an AI to write reference docs for a custom extension to a rather niche, obscure scripting language (JsonLogic).]]></summary></entry></feed>