<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Havoric Journal</title><link>https://havoric.com/journal/</link><description>Notes from the Havoric studio on AI automation, shipping software fast, and what to build (or not build).</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><atom:link href="https://havoric.com/journal/index.xml" rel="self" type="application/rss+xml"/><item><title>The review queue: keeping humans in the loop</title><link>https://havoric.com/journal/the-review-queue/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://havoric.com/journal/the-review-queue/</guid><description>The pattern behind every automation we ship: let software handle the 90% it's sure about, and give a human a fast, boring queue for the rest.</description><content:encoded><![CDATA[<p>Every automation we ship has the same shape at its core. Software handles the cases it is confident about, and everything else lands in a queue for a human to clear. Not because the AI can&rsquo;t guess — because some mistakes are too expensive to make silently.</p>
<p>This post is the pattern in full, with the actual code shapes we use. It is not clever. That is the point.</p>
<h2 id="the-shape-of-the-thing">The shape of the thing</h2>
<p>Three parts, always the same:</p>
<ol>
<li><strong>A classifier</strong> that scores each item and decides: act, or escalate.</li>
<li><strong>A queue</strong> that holds escalated items with everything a reviewer needs on one screen.</li>
<li><strong>A feedback loop</strong> — every human decision becomes a labelled example for the next tuning pass.</li>
</ol>
<p>The threshold between &ldquo;act&rdquo; and &ldquo;escalate&rdquo; is a business decision dressed up as an engineering one. It comes straight from the cost of a mistake:</p>
<table>
	<thead>
			<tr>
					<th>Step</th>
					<th>Cost of a wrong call</th>
					<th>Threshold</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Tagging a support ticket</td>
					<td>Minutes of misrouting</td>
					<td>Act above 0.7</td>
			</tr>
			<tr>
					<td>Matching an invoice to a PO</td>
					<td>An awkward supplier email</td>
					<td>Act above 0.9</td>
			</tr>
			<tr>
					<td>Issuing a refund</td>
					<td>Real money, twice</td>
					<td>Always escalate</td>
			</tr>
	</tbody>
</table>
<h2 id="the-classifier">The classifier</h2>
<p>The scoring call is deliberately dull. One function, one structured answer, no streaming, no agents:</p>
<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">anthropic</span> <span class="kn">import</span> <span class="n">Anthropic</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">Anthropic</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">classify</span><span class="p">(</span><span class="n">invoice_text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">po_candidates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Match an invoice to a PO, or admit we can&#39;t.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">messages</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">model</span><span class="o">=</span><span class="s2">&#34;claude-sonnet-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">max_tokens</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">system</span><span class="o">=</span><span class="n">SYSTEM_PROMPT</span><span class="p">,</span>  <span class="c1"># the rules a human would state, verbatim</span>
</span></span><span class="line"><span class="cl">        <span class="n">messages</span><span class="o">=</span><span class="p">[{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">render_prompt</span><span class="p">(</span><span class="n">invoice_text</span><span class="p">,</span> <span class="n">po_candidates</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><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">parse_json</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">content</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># result: {&#34;po_number&#34;: &#34;PO-4471&#34;, &#34;confidence&#34;: 0.94, &#34;reason&#34;: &#34;...&#34;}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span>
</span></span></code></pre></div><p>Two details that matter more than the model choice:</p>
<ul>
<li>The system prompt is the rules from the scoping call, written down. When the client says &ldquo;we never auto-post invoices over €10,000&rdquo;, that sentence goes in verbatim.</li>
<li>The model must be allowed to say <em>I don&rsquo;t know</em>. A forced choice with no escape hatch is how confident nonsense ends up in your accounting system.</li>
</ul>
<h2 id="the-queue">The queue</h2>
<p>The queue is a table. Resist making it more than a table.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">review_queue</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w">           </span><span class="nb">bigint</span><span class="w"> </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">IDENTITY</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">item_type</span><span class="w">    </span><span class="nb">text</span><span class="w">        </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">   </span><span class="c1">-- &#39;invoice&#39;, &#39;ticket&#39;, ...
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">payload</span><span class="w">      </span><span class="n">jsonb</span><span class="w">       </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">   </span><span class="c1">-- everything the reviewer sees
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">suggestion</span><span class="w">   </span><span class="n">jsonb</span><span class="w">       </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">   </span><span class="c1">-- what the model would have done
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">confidence</span><span class="w">   </span><span class="nb">numeric</span><span class="w">     </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">status</span><span class="w">       </span><span class="nb">text</span><span class="w">        </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">decided_by</span><span class="w">   </span><span class="nb">text</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">decided_at</span><span class="w">   </span><span class="n">timestamptz</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">created_at</span><span class="w">   </span><span class="n">timestamptz</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">now</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>The reviewer sees the item, the model&rsquo;s suggestion, and its reasoning — and makes one of two moves: <strong>approve</strong> the suggestion or <strong>correct</strong> it. One keystroke each. If clearing the queue takes more than a few seconds per item, people stop clearing it, and the whole system quietly rots.</p>
<blockquote>
<p>A review queue nobody clears is worse than no automation at all: the work still isn&rsquo;t done, and now everyone believes it is.</p>
</blockquote>
<h2 id="the-feedback-loop">The feedback loop</h2>
<p>Every decision is a labelled example. Once a month, we look at where humans overrode the model and tune — usually the prompt, occasionally the threshold, rarely the model:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># tuning-run.yaml — inputs to a monthly review pass</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">window</span><span class="p">:</span><span class="w"> </span><span class="l">30d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">export</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">decisions_where</span><span class="p">:</span><span class="w"> </span><span class="l">status = &#39;corrected&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">decisions_where</span><span class="p">:</span><span class="w"> </span><span class="l">confidence &gt; 0.9 AND status = &#39;corrected&#39; </span><span class="w"> </span><span class="c"># the scary ones</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">review</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">prompt_rules     </span><span class="w"> </span><span class="c"># do the written rules cover the misses?</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">threshold        </span><span class="w"> </span><span class="c"># is 0.9 still right for this cost of error?</span><span class="w">
</span></span></span></code></pre></div><p>The metric we watch is not accuracy. It is <strong>escalation rate over time</strong>. A healthy system starts around 20–30% escalated and drifts down as the rules sharpen. If it drifts up, the process changed and nobody told the software — which is exactly what the queue is for catching.</p>
<h2 id="where-this-leaves-you">Where this leaves you</h2>
<p>Ninety percent automatic with a fast human queue beats one hundred percent automatic in every deployment we have done. The queue is not a compromise on the way to &ldquo;full&rdquo; automation — it is the finished state, and the reason the system is still trusted a year later.</p>
<p>If you&rsquo;re weighing a process of your own, the <a href="/journal/should-you-automate-that-process/">five-question checklist</a> is the place to start — question four is the one this whole post hangs off.</p>
]]></content:encoded></item><item><title>Should you automate that process?</title><link>https://havoric.com/journal/should-you-automate-that-process/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://havoric.com/journal/should-you-automate-that-process/</guid><description>The five-question checklist we run on every scoping call — half the time the answer is "not yet."</description><content:encoded><![CDATA[<p>Half the calls we take end with us saying &ldquo;don&rsquo;t automate this yet.&rdquo; That surprises people. Here is the checklist we run before we quote anything — you can run it yourself in ten minutes.</p>
<h2 id="01--does-it-happen-at-least-weekly">01 — Does it happen at least weekly?</h2>
<p>Frequency is the whole economics of automation. A task done 200 times a week pays back a build in months. A quarterly report, almost never. Count the repetitions honestly — including the times someone does it wrong and redoes it.</p>
<h2 id="02--can-you-write-the-rules-down">02 — Can you write the rules down?</h2>
<p>If an experienced person can explain how they decide — &ldquo;if the invoice matches a PO, post it; if not, flag it&rdquo; — software can do it, and AI handles the messy middle: reading PDFs, interpreting emails, extracting fields. If nobody can explain the decision, fix the process before you automate it.</p>
<h2 id="03--is-the-data-reachable">03 — Is the data reachable?</h2>
<p>Automation needs a way in and a way out. Email inboxes, spreadsheets, and most modern tools are easy. A desktop app from 2009 with no export is not impossible — but it moves the quote. Know where your data lives before the call; it&rsquo;s the first thing we&rsquo;ll ask.</p>
<h2 id="04--what-does-a-mistake-cost">04 — What does a mistake cost?</h2>
<p>This decides how much human review stays in the loop. A mistagged support ticket costs minutes; a wrong payment costs real money. High-stakes steps keep a human approval; low-stakes steps run straight through. Most good systems are 90% automatic with a small review queue — like the flagged invoice in our own pipeline.</p>
<h2 id="05--will-anyone-own-it">05 — Will anyone own it?</h2>
<p>Software needs one person on your side who cares that it keeps running — who reviews the flagged items and tells us when the process changes. Without an owner, the best automation quietly dies in six months. Name that person before you build.</p>
<blockquote>
<p><strong>The short version.</strong> Weekly or more · rules you can state · reachable data · known cost of error · a named owner. Score four out of five and it&rsquo;s probably worth a call.</p>
</blockquote>
<p>If you scored your process and it passed, the next step is the same one every project here starts with: a <a href="https://cal.com/havoric/intro">30-minute call</a> where you walk us through it. We&rsquo;ll tell you honestly if it&rsquo;s a no — that&rsquo;s half the calls, remember.</p>
]]></content:encoded></item></channel></rss>