<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nabbil Khan</title>
    <description>The latest articles on DEV Community by Nabbil Khan (@nabbilkhan).</description>
    <link>https://dev.to/nabbilkhan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035494%2Ff2c8e058-05be-47d8-b807-d66bd315d13f.jpeg</url>
      <title>DEV Community: Nabbil Khan</title>
      <link>https://dev.to/nabbilkhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nabbilkhan"/>
    <language>en</language>
    <item>
      <title>Prefer Loud Bugs</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:03:17 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/prefer-loud-bugs-flf</link>
      <guid>https://dev.to/nabbilkhan/prefer-loud-bugs-flf</guid>
      <description>&lt;p&gt;The rule took four minutes to write. If a contact already has an open opportunity, do not make a second one, leave a note instead. Nobody argues with a rule like that. Then I opened the CRM it writes into and counted twenty open opportunities, all created in the last ten weeks, and not one of them marked won or lost in the entire history of the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field had never moved
&lt;/h2&gt;

&lt;p&gt;Sit with that for a second, because it took me a while. My rule says skip while the opportunity is open. The data says opportunities here do not close. Nothing has ever gone from open to anything else. So open is not a stage. It is a permanent property of every record anyone has ever made.&lt;/p&gt;

&lt;p&gt;Which means I did not write a dedupe rule. I wrote a rule that mutes a person for life the first time they fill in the form. After that, the only thing the agent ever does for them is leave a note, and notes get read by nobody. I built a machine whose failure mode is silence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; A rule that keys on a status field is a bet that somebody will eventually change that field, and when nobody does, your agent goes quiet instead of loud.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the part worth keeping. Loudly wrong is fine. Loudly wrong gets caught in a day, because somebody gets two emails and tells you. Quietly wrong gets caught never. No one writes in to complain that the agent did not contact them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20&lt;/strong&gt; open opportunities in the account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10&lt;/strong&gt; weeks since the oldest one opened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; ever marked won or lost, all time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Same bug, different industry
&lt;/h2&gt;

&lt;p&gt;I am writing this down instead of just fixing it because I have hit the same thing in businesses that have nothing to do with sales.&lt;/p&gt;

&lt;p&gt;In the reimbursement engine I run for remote therapeutic monitoring, each patient gets a green dot for the month once a live call happens. The check is an EXISTS against the billable ledger. Same shape as the dedupe rule: look at a field, decide whether to act. It works, and it works for a boring reason. Somebody logs a call every month, the row appears, the dot flips. The state moves, so the check means something.&lt;/p&gt;

&lt;p&gt;Now the other side. In the denial engine we queue batch jobs and hand the caller a URL to poll. For a while the local worker only woke up when something read the jobs collection. A client doing exactly what our docs said (post a batch, poll the URL you were given) never touched that collection. So the job sat there. No error. No 500. Just a status field that would have changed, if anything had ever come along to change it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The bug was never in the rule. It was in assuming somebody keeps the field honest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three industries, one mistake. B2B sales, clinical billing, claims automation. Every time I wrote a condition against a field and quietly assumed a person or a cron job would keep that field true.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I shipped a dedupe rule against a status field that had never once changed value, and the cost was invisible: people who wrote in, got a note, and never heard from us again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Go look at the field before you key on it
&lt;/h2&gt;

&lt;p&gt;The fix is not clever. Before you write a condition against a state field, go read the history of that field. Not the schema. Not what the sales rep says the process is. The actual spread of values across the actual rows.&lt;/p&gt;

&lt;p&gt;One query. How many records sit in each state, and when did any of them last move? If everything is in one bucket and nothing has moved in ninety days, that field is not a state machine. It is a label somebody applied once and forgot. You cannot hang a decision on it.&lt;/p&gt;

&lt;p&gt;That question saved me again last month in the quoting engine, where I almost gated follow up on a quote status nobody updates after they send the PDF.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The monthly check in the billing engine holds up for one boring reason: a human logs a live call every month, so the ledger really changes, and an EXISTS against it means something.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Look at the distribution before you key on the state. If one value holds every row, it is not a state.&lt;/li&gt;
&lt;li&gt;Ask when the field last changed, not what it is supposed to mean.&lt;/li&gt;
&lt;li&gt;Prefer time to status. No outreach in 30 days fails softly. No open opportunity can hang forever.&lt;/li&gt;
&lt;li&gt;Give every skip a loud path. If the agent has said nothing about a contact for a month, that is an alert, not a note.&lt;/li&gt;
&lt;li&gt;Make silent failures expensive to ignore. What you cannot see, you will not fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Loud is a feature
&lt;/h2&gt;

&lt;p&gt;I have spent years making agents careful. Guard the submit. Do not double up. Check before you act. That instinct is right, and it is also how you end up with a system that never does anything wrong and never does anything at all.&lt;/p&gt;

&lt;p&gt;A duplicate email costs you a bad afternoon. A muted prospect costs you a quarter you never find out about. When you are picking between the two, pick the noisy one. You can always turn the volume down later.&lt;/p&gt;

&lt;p&gt;Go look at your state fields. Not the code. The data.&lt;/p&gt;

&lt;p&gt;What state field in your stack does everyone trust, that nobody actually updates?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/prefer-loud-bugs" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>agentdesign</category>
      <category>b2bsales</category>
    </item>
    <item>
      <title>The Quota Picked the Model</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:04:24 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/the-quota-picked-the-model-hc5</link>
      <guid>https://dev.to/nabbilkhan/the-quota-picked-the-model-hc5</guid>
      <description>&lt;p&gt;A line in my denial engine said the model was Sonnet. Every assessment that engine has ever served ran on Haiku. Nobody edited the config, nobody shipped the wrong build, and the line was true the day I wrote it. It just was not true any time the thing ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number was real, the name was not
&lt;/h2&gt;

&lt;p&gt;The engine takes a denial, reads the codes and the payer language, and scores the odds of winning the appeal. It has a primary model and a fallback. Primary was Sonnet. When the primary comes back with a 429, the code drops to Haiku and keeps going, which is what you want at 2 AM CT with four hundred claims to clear before the morning.&lt;/p&gt;

&lt;p&gt;On my key, Sonnet 429s. Not sometimes. Every time. So the fallback was never a fallback. It was the road. The primary was a door that had been locked since before I started measuring anything.&lt;/p&gt;

&lt;p&gt;I found it while chasing something else, and then I had to go back and re-label the whole eval set.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;94&lt;/strong&gt; eval cases scored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100%&lt;/strong&gt; of them answered by the fallback model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400&lt;/strong&gt; claims in a single overnight batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; lines of code that had to change to swap models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers were fine. They came from real runs on real denials, and I still trust them. What was wrong was the sentence I kept saying out loud about them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; For weeks I called the engine's accuracy a Sonnet number in rooms where the difference mattered, and every one of those numbers came from a model I had never once said the name of.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The upgrade is the regression
&lt;/h2&gt;

&lt;p&gt;Here is the part that should bother you more than the wrong name.&lt;/p&gt;

&lt;p&gt;Think about who actually gets Sonnet. Not me. A client on a paid key with real headroom, whose requests never hit a limit, gets the primary on every call. The person with the better account is the only one running the model I never tested. Same commit, same container, same config file. The code did not change. The quota did.&lt;/p&gt;

&lt;p&gt;We are trained to worry about the other direction. You picture the slow machine, the bad network, the throttled key, and you make the thing degrade gracefully. Nobody writes an eval for degrading upward.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A rate limit is a config value, so an eval score belongs to the account that produced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; An eval score is a fact about the account that ran it and not about the code, so the moment you hand that code to someone else the score quietly stops being true.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I pinned it. claude-haiku-4-5-20251001, spelled out with the date, because Haiku is what I actually validated. If I want Sonnet numbers I can go get Sonnet numbers, on a key where Sonnet answers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The engine now runs the model its numbers came from on every account, and each assessment records which model replied instead of which one was configured.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same bug in other rooms
&lt;/h2&gt;

&lt;p&gt;Once you see this shape you cannot stop seeing it. I run systems in a few different industries, and it turned up in all of them inside one week.&lt;/p&gt;

&lt;p&gt;An appeal deadline stored as 2026-10-13 rendered as October 12 on a laptop in Chicago. The date in the database was right. The reader's clock decided what it said. Anyone west of UTC lost a day on a deadline with money attached to it.&lt;/p&gt;

&lt;p&gt;On the Medicare remote-monitoring billing platform, invite and password-reset mail never reached the testers. The code was correct. The sending account sat in a sandbox that only delivers to verified addresses, and mine was verified, so it worked perfectly for the one person checking whether it worked.&lt;/p&gt;

&lt;p&gt;In the same denial engine, the local batch worker only woke up when something asked for the jobs list. A caller following the documented flow, post a batch and poll the URL you are handed, never touched that route. The job sat queued forever while the code was, technically, doing its job.&lt;/p&gt;

&lt;p&gt;On the local box that serves open source models, which model answers depends on what fits in memory that night. The config has an opinion. Memory has the last word.&lt;/p&gt;

&lt;p&gt;Different systems, one shape. The code is identical on every machine. The behavior gets picked by things nobody wrote down: a quota, a sandbox flag, a clock, a route somebody happened to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pin the model. A fallback that fires every time is not a fallback, it is your default in a costume.&lt;/li&gt;
&lt;li&gt;Log what answered, not what was configured. The record should say Haiku because Haiku replied, not because a variable claimed so.&lt;/li&gt;
&lt;li&gt;Treat the account as an input. Quota, verified senders, region, sandbox state, clock. All of it is config you never wrote and will never see in a diff.&lt;/li&gt;
&lt;li&gt;Re-run the evals where the code is going to run. A score from my key is a fact about my key.&lt;/li&gt;
&lt;li&gt;Ask which way a difference fails. If the better environment is the untested one, the upgrade is the regression.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used to think the code was the product and the environment was setup. It is closer to the other way around. The commit says what the system is supposed to do. The account it runs on decides what it does.&lt;/p&gt;

&lt;p&gt;If you have a number you say out loud, go look at what actually answered. You can read every line and still not know. Some of it is written somewhere else.&lt;/p&gt;

&lt;p&gt;Which environment fact, a quota, a region, a sandbox flag, has changed your system's behavior without a line of code changing?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/the-quota-picked-the-model" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>aiagents</category>
      <category>evals</category>
    </item>
    <item>
      <title>Most of It Says No</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:10:55 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/most-of-it-says-no-2idp</link>
      <guid>https://dev.to/nabbilkhan/most-of-it-says-no-2idp</guid>
      <description>&lt;p&gt;I went looking for the model call in our denial agent last month and it took me four minutes to find it. One function, about forty lines. Everything around it, eleven files, exists to decide whether that function is allowed to run at all. I had built what I thought was an AI system, and most of it was a system for not using AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape in three places
&lt;/h2&gt;

&lt;p&gt;I run software in industries that have nothing to do with each other. One system reads insurance denials and drafts appeals. One prices water features for contractors, the pump, the liner, the rock, the labor. One calls insurers and waits on hold for forty minutes so a person does not have to. Different buyers, different rules, different words for everything.&lt;/p&gt;

&lt;p&gt;They came out the same shape. Each one has a small part that produces an answer and a large part that decides whether the answer is allowed to leave. I did not design that. I noticed it the third time.&lt;/p&gt;

&lt;p&gt;It goes further down than I expected. On my own hardware I run open models, Qwen and DeepSeek, and their main job is not to answer anything. They read the input and decide whether the expensive model ever sees it. A gate in front of a gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What guessing costs
&lt;/h2&gt;

&lt;p&gt;Early on I let the model do arithmetic in the quoting engine. It read the job, picked the parts, added up the bill of materials, and produced a number that looked completely reasonable. It was low by about nine percent. In construction, low is the direction that takes money out of the contractor's pocket, and nobody notices until the hole is already dug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; A quote went out under cost because I let a language model add. The number looked right, which is exactly why nobody checked it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pricing moved into plain code that afternoon. The model picks parts and writes the description. Multiplication belongs to a function that cannot be talked into anything.&lt;/p&gt;

&lt;p&gt;The denial agent taught me the same lesson in a different accent. It once wrote a careful, well argued appeal that cited a document we did not have. Nothing was wrong with the writing. The rule now is boring: no source in hand, no sentence on the page. I did a Master's in Language and Theology before I wrote a line of production code, and the useful thing that training gives you is a nose for the gap between what a text says and what you wish it said. That turns out to be most of the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abstention is a dial
&lt;/h2&gt;

&lt;p&gt;Everyone tunes accuracy. Almost nobody publishes the other number, which is how often the system declines to answer. The two move together, and the second one is the one you actually control. Our denial classifier runs at ninety percent on the cases it keeps, and it keeps about seven out of eight. I can push coverage to everything and watch accuracy fall, or refuse more and look useless. The work is choosing the point and being able to defend it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;90.0%&lt;/strong&gt; accuracy on the denials it keeps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 in 8&lt;/strong&gt; cases handed to a person instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; independent checks before a quote can leave&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dial only matters if the refusal has somewhere to go. This is the part I got wrong for a year. A system that stops is not automatically a safe system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the machine stops and nobody gets the work, you have not built caution. You have built a hole.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So every refusal now has an address. The denial agent hands the case to a biller with the reason attached. The voice agent drops the call into a human queue with the recording and the time in Central. The quoting engine flags the line item and will not render a PDF until someone signs off on it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Since pricing left the model, no quote has gone out under cost, and the hard jobs land in a review queue instead of a customer's inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The part of an AI system that decides not to answer is what makes the rest of it safe to put in front of anyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to build first
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write the refusal path before the happy path. It is harder, and you will not go back and do it later.&lt;/li&gt;
&lt;li&gt;Give every refusal an address: a queue, a person, a fallback. Silence is not a destination.&lt;/li&gt;
&lt;li&gt;Take arithmetic away from the model. Anything with a right answer belongs in code.&lt;/li&gt;
&lt;li&gt;Report coverage next to accuracy. One number without the other is marketing.&lt;/li&gt;
&lt;li&gt;Watch the gate, not the model. The gate is the part that fails quietly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is clever. It is the least interesting code I write, and it is the reason the interesting code is allowed near a customer at all. The people shipping demos are tuning the model. The people running things are mostly building the part that says no, and they are mostly doing it alone, which is a shame, because the shape is the same in every industry I have worked in. If you are building that part, I would like to compare notes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/most-of-it-says-no" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aievals</category>
      <category>aisystems</category>
      <category>pharmaservices</category>
      <category>proposalsandrfps</category>
    </item>
    <item>
      <title>Nobody Asked the Model</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:06:55 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/nobody-asked-the-model-icn</link>
      <guid>https://dev.to/nabbilkhan/nobody-asked-the-model-icn</guid>
      <description>&lt;p&gt;For months my eval dashboard said my skills scored 0 percent, and I believed it. Zero is a believable number for a young system. So I rewrote prompts. I restructured skills. I tightened trigger phrases, reran the suite, and watched the zero not move. The whole time, no model was ever asked a single question. The harness had quietly swapped credentials on me. Every request died at the front door with a 401, and every 401 was scored as a wrong answer. I spent months improving the answers of a model nobody asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line of fallback
&lt;/h2&gt;

&lt;p&gt;The bug was one line. Line 90 of run_eval.py had a fallback: if no API key was set, use the OAuth token from my Claude Code session as an API key. It looks harmless. It is the kind of line you write at midnight so a script will run on a fresh machine. But an OAuth token is not an API key. The API rejected every request with a 401, the harness caught the failure, and it had nowhere to put it except the answer column. Wrong credential became wrong answer. Wrong answer became 0 percent. And 0 percent became a fact I planned around.&lt;/p&gt;

&lt;p&gt;What should have tipped me off was that the number never moved. Real evals are noisy. Skills improve a little, regress a little, flake on one case out of twenty. A metric that sits at exactly zero for months is not describing a model. It is describing something upstream that fails the same way every time. Flat numbers are plumbing numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0%&lt;/strong&gt; reported skill accuracy, for months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401&lt;/strong&gt; what every eval case actually returned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; line of fallback code behind the whole number&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; questions any model was asked&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I treated 0 percent as a fact about my skills for months. I rewrote prompts and restructured workflows to move a number that was actually about a credential. The fix was deleting one line, and the score I had been chasing had never once been about the work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two kinds of fallback
&lt;/h2&gt;

&lt;p&gt;I build and run AI systems in several industries: a medical billing operation with a denial engine, an autonomous ad operator named Mia, a quoting engine for water-feature contractors, and the harness that evals my own agents. The same shapes keep turning up in all of them, and this bug taught me to sort fallbacks into two kinds.&lt;/p&gt;

&lt;p&gt;A fallback in a compute path costs you quality. The big model times out, so you route to a smaller one. The cache misses, so you recompute. You get a worse answer or a slower one, but it is still an answer about the right subject. Degrading there is fine. Degrading there is the point.&lt;/p&gt;

&lt;p&gt;A fallback in an identity path costs you the truth. Identity is the part of the system that says who is talking: which credential, which user, which account. When that part fails and something helpfully substitutes a different identity, nothing crashes. The run finishes and hands you a confident number about the wrong subject. My 0 percent was not a measurement of my skills. It was a measurement of a token, laundered through an eval harness until it looked like a measurement of my skills.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; A fallback in a compute path costs you quality. A fallback in an identity path costs you the truth, because the run still finishes and hands you a confident number about the wrong subject.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same shape in an approvals dashboard
&lt;/h2&gt;

&lt;p&gt;A while later the same shape showed up in my ad operation. Mia is my autonomous advertising operator. Mia drafts the moves, and a human approves the risky ones through a dashboard behind Cloudflare Access, which stamps every request with a JWT that says who you are. The tempting fallback was obvious: if the JWT is missing, fall back to a session cookie so the dashboard keeps working.&lt;/p&gt;

&lt;p&gt;I said no, and the eval bug is why. An approval attributed to a person who did not approve is worse than a login screen. A login screen costs you a minute. A false approval is a record that lies, and it lies in the one place you will later go to find out what happened. When identity fails, the only honest state is blocked. So the dashboard fails closed. No JWT, no page.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The Mia approvals dashboard fails closed behind Cloudflare Access: no JWT, no page. Every approval in its history is attached to a person who actually clicked it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule under both systems is the same one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A 401 is not a wrong answer, and a missing JWT is not an approval.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An infrastructure failure is not a verdict about the work. My denial engine draws the line in the same place: its audit trail records which tools actually ran, never which tools the model claims it ran. It is the same line, drawn in three different industries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask who answered
&lt;/h2&gt;

&lt;p&gt;The fix took five minutes. I deleted the fallback and made the script refuse to run without a real key. Auth errors now kill the run loudly instead of flowing into the score. The first honest eval was strange to watch, because the skills were fine. They had always been fine. Nobody had asked them anything.&lt;/p&gt;

&lt;p&gt;What I took from it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before you ask what a number means, ask who answered. Sometimes nobody did.&lt;/li&gt;
&lt;li&gt;Fall back on compute. Fail closed on identity.&lt;/li&gt;
&lt;li&gt;Keep error channels out of the answer column. A 401 is infrastructure, not a verdict.&lt;/li&gt;
&lt;li&gt;Distrust flat metrics. Real systems are noisy, and a number that never moves is usually measuring the plumbing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you run agents in production, go read your fallbacks. Most of them are fine. You are looking for the ones that swap who is talking, because those will hand you a confident number about the wrong subject, and you will believe it, like I did. The skills were never the problem. Nobody asked the model.&lt;/p&gt;

&lt;p&gt;Where in your stack does a failed credential quietly turn into a different answer?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/nobody-asked-the-model" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>advertising</category>
      <category>agentinfrastructure</category>
      <category>aitooling</category>
      <category>evals</category>
    </item>
    <item>
      <title>The Card Said AI</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:03:12 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/the-card-said-ai-1hbo</link>
      <guid>https://dev.to/nabbilkhan/the-card-said-ai-1hbo</guid>
      <description>&lt;p&gt;The card said to use AI to build a quarterly compliance report out of a client's event platform. Before I wrote a line I cloned the app. Laravel, about 22,000 files. I spent a day reading it instead of building. There was already a scheduled tracker running. There were attendee exports nobody opened. There were seventeen compliance sheets sitting in the repo that nobody had connected to the ask. The report was most of the way built. It just had no name and no owner, so from the outside it looked like it did not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the card actually meant
&lt;/h2&gt;

&lt;p&gt;The card said AI. What the client meant was that the quarter closes and somebody sits down for three days with spreadsheets and dreads it. Nobody wants a model. They want the dread to stop. Those are different deliverables, and if you take the first one at face value you will build a beautiful thing that solves nothing.&lt;/p&gt;

&lt;p&gt;So I did the boring part. I took the compliance template, all 147 columns of it, and went column by column against what the platform already held. Forty seven mapped straight across, same field, different name. Sixty one were derivable, a join or some date math or a rollup away. Thirty nine were real gaps, things the platform never collected at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;147&lt;/strong&gt; template columns triaged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;47&lt;/strong&gt; mapped directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61&lt;/strong&gt; derivable from existing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;39&lt;/strong&gt; real gaps in the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The triage took longer than the code. And when it was done there was nothing left for a model to do. The mappings were fixed. The derivations were arithmetic. The gaps were gaps, and no model can invent a field nobody collected. It can only write a confident sentence where the field should be.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The mapping was not ambiguous. It was just unknown, and those two feel identical until somebody does the work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I took the model out
&lt;/h2&gt;

&lt;p&gt;I had already wired one in. Pulling it back out felt like losing. Then I asked the question that decides these things: does this report have to come out the same way twice?&lt;/p&gt;

&lt;p&gt;Yes. Somebody pulls a compliance report a year later, somebody who was not in the room, and it has to match the one that was filed. Not roughly. Exactly. A model that is right 97 percent of the time is a model that will quietly contradict last quarter's filing, and you find out in an audit.&lt;/p&gt;

&lt;p&gt;So it became a fixed alias table and a hash chained log. Every row traces back to the record it came from. Every run leaves a chain you can check. Boring. Replayable. When the auditor asks where a number came from, the answer is a row, not a probability.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; If the same input has to give the same answer twice, you do not want a model, you want a table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I built the model path first because the card said AI, and I threw that day away. I should have read the codebase before I read the requirements.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same shape keeps showing up
&lt;/h2&gt;

&lt;p&gt;I keep running into this in industries that have nothing to do with each other. In healthcare the sharpest thing I shipped this month was not a model at all. It was a QR code that opens the patient app with no download and no password, plus one slide that says plainly which decisions we still need from the doctor. In pharma compliance it was a lookup table. In the medical billing work the model earns its keep in exactly one place, reading the messy denial letter, and everything downstream of that is deterministic on purpose.&lt;/p&gt;

&lt;p&gt;The rule underneath is simple. Use a model where the input is genuinely unstructured and the output is allowed to vary. Everywhere else it is an expensive way to build a system you can no longer explain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Shipped the quarterly report with no model in it, fully replayable, and handed over the 39 real gaps as a list the client can go fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I would do next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the codebase before you read the requirements. Half of what you are asked to build is already there under a different name.&lt;/li&gt;
&lt;li&gt;Do the triage by hand, once, column by column. It is slow, and it is the only thing that tells you whether the problem is hard or just unmapped.&lt;/li&gt;
&lt;li&gt;Ask whether the output has to replay. If it does, make it deterministic.&lt;/li&gt;
&lt;li&gt;Take the model out when the triage kills it, even though you built it and it feels like waste.&lt;/li&gt;
&lt;li&gt;Hand over the gaps as a list. The 39 things the data does not contain are worth more than the 108 you automated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes the whole contribution is proving the AI was not needed. You cannot bill for that easily and it does not demo well. But the client stops dreading the quarter, and the report replays the same way in an audit two years out. That is what they were asking for the whole time. They just used the only word anyone gives them for it.&lt;/p&gt;

&lt;p&gt;What is your test for when a workflow is genuinely unstructured enough to deserve a model?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/the-card-said-ai" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>compliance</category>
      <category>consulting</category>
      <category>determinism</category>
      <category>pharma</category>
    </item>
    <item>
      <title>Tools an Agent Can't Use</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Mon, 20 Jul 2026 14:05:19 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/tools-an-agent-cant-use-mfm</link>
      <guid>https://dev.to/nabbilkhan/tools-an-agent-cant-use-mfm</guid>
      <description>&lt;p&gt;Last month I tried to fire Webflow. Not because it was slow, and not because it was expensive. The nonprofit site it runs, muslimscholars.us, was doing fine. I tried to fire it because its Designer is a canvas, and a canvas is a thing you drive with your hands. My workforce does not have hands. I run my companies with AI agents now, and any tool an agent cannot operate quietly turns back into my job.&lt;/p&gt;

&lt;h2&gt;
  
  
  A canvas with no door
&lt;/h2&gt;

&lt;p&gt;Here is what I mean by cannot operate. Webflow has an API, but the API stops at content. It will update a blog post all day. It cannot move a div. The design itself, the layout, the classes, the breakpoints, lives inside the Designer, and the Designer is a rendered canvas. There is no API into it. So I tried browser automation next, the way you would drive any stubborn web app, and it bounced off. The canvas is a picture of the design, not the design. There was nothing underneath to click.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I spent days pointing browser automation at that canvas before I admitted defeat. Every selector I could reach was part of the frame, not the picture. The tool was not broken. It was sealed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the site is moving to code. Not because code is a better site builder. In most ways it is a worse one. But code is the one interface every agent already speaks. A change becomes a diff, and a diff can be reviewed, tested, and reverted by something that never sleeps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching Unity to work blind
&lt;/h2&gt;

&lt;p&gt;A few days later I did the opposite. I build a VR app that trains pharmacy technicians. It runs on Quest, which means it is built in Unity, and Unity is a giant GUI editor. By the Webflow test it should have been next out the door. But I kept it, because what my agents actually needed from Unity was never the editor. It was one answer, over and over: does this C# still compile against this project? The editor knows, because the editor holds the real reference assemblies. So I pointed Roslyn at those assemblies directly, on a server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; A headless server with no screen, no GPU, and no Unity license now typechecks the Quest app in half a second. Agents edit the C#, get compiler truth back instantly, and never open the editor at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One rule, two directions
&lt;/h2&gt;

&lt;p&gt;Two opposite moves in the same week, in two businesses that share nothing: a nonprofit's website and a pharmacy training app in VR. That is the part that surprised me. The rule was the same both times. The difference is where the value lives. Webflow's value is the canvas; take that away and there is nothing left to wrap. Unity's value sits under the canvas: the compiler, the assemblies, the build pipeline. The GUI is just the lid. When the value is under the lid, you pry the lid off. When the value is the lid, you leave.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool only I can drive is not a tool anymore. It is a queue with one worker, and the worker is me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; When your workforce is agents, the question that decides whether a tool stays is not what it can do. It is whether something that is not you can operate it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0.5s&lt;/strong&gt; Unity typecheck on a headless server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; APIs into the Webflow Designer canvas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; opposite fixes, one rule, one week&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used to judge software the way everyone does: features, price, polish. That list still matters. But it now has a gate in front of it, and most tools I look at do not get past the gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ask now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ask who, or what, can operate it. If the answer is only you, it is a bottleneck wearing a nice interface.&lt;/li&gt;
&lt;li&gt;Find where the value lives. Under the GUI, wrap it. In the GUI, replace it.&lt;/li&gt;
&lt;li&gt;Do not settle for "has an API." Check that the API reaches the part the tool is actually for. Webflow has one. It just does not touch the thing Webflow is for.&lt;/li&gt;
&lt;li&gt;Treat headless as a requirement, not a feature. It is the difference between a tool and a job.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strange part is that neither tool changed. Webflow is the same product it was a year ago, and so is Unity. What changed is who shows up for work. Software has always competed on what it can do. It is starting to compete on what can do it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/tools-an-agent-can-t-use" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>crossindustry</category>
      <category>nonprofitweb</category>
      <category>pharmacyeducation</category>
    </item>
    <item>
      <title>Zero Is Not a Score</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sun, 19 Jul 2026 00:20:18 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/zero-is-not-a-score-e02</link>
      <guid>https://dev.to/nabbilkhan/zero-is-not-a-score-e02</guid>
      <description>&lt;p&gt;The evals for my agent skills scored 0% for as long as I had records. Not low. Not noisy. Exactly zero, every skill, every run. And I believed it. For months I thought my skills were bad, because the number said so and the number never wavered. Then one night I actually read the harness. It had fallen back to the wrong auth token. Every call it made came back 401, and it quietly graded each one a failure. The skills never got a chance to fail on their own. I was not measuring them at all. I was reading a broken thermometer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real weakness is jagged
&lt;/h2&gt;

&lt;p&gt;Here is what took me too long to see. When a system is genuinely bad, it scores 40% one week and 60% the next. It passes the easy cases and trips over the hard ones. It has good days. Incompetence has texture, because an incompetent system is still in contact with the world, and the world varies.&lt;/p&gt;

&lt;p&gt;A flat number has no texture. A flat number means the measurement stopped touching the thing being measured somewhere upstream, and what you are reading is the instrument's resting state. Doctors know this. A heart monitor drawing a perfectly straight line does not mean the patient is calm.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only a broken thermometer writes the same number every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; A performance number with no variance is a reading of the instrument, not of the thing being measured.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same bug in three industries
&lt;/h2&gt;

&lt;p&gt;I run systems in advertising, in healthcare billing, and in agent operations, and the same shape shows up in all of them. In advertising I found a dashboard figure that had been hardcoded for two years. Nobody questioned it, because it looked right, and it looked right because it never moved. In agent operations, an account-rotation bug in one of my pipelines overwrote every real error with the same generic message, "no active accounts," so for a while every distinct failure in that system looked identical. And in the denial-assessment engine I run for a medical-billing operation, an agreement metric came back at 44.7%, alarmingly low, until I noticed the two sides of the comparison were scored in different units. One side used an equivalence map; the other compared raw strings. Twenty-seven of the "disagreements" were the same operational decision written two ways. Rescored in units that meant something, the engine sat at 92.0% on classification and 90.7% on action against the golden set.&lt;/p&gt;

&lt;p&gt;Three industries, one pattern. When a number is perfectly flat, perfectly round, or perfectly terrible, the odds you are looking at the system go down, and the odds you are looking at the pipe go way up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0%&lt;/strong&gt; skill eval score, every run on record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401&lt;/strong&gt; the status code behind every one of those zeros&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44.7%&lt;/strong&gt; an agreement score that was really a units mismatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;90.7%&lt;/strong&gt; action accuracy once the eval scored meaning&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a healthy number looks like
&lt;/h2&gt;

&lt;p&gt;The week after I fixed that eval, the numbers got interesting. Interesting is the point. Mapped-action accuracy on the golden set went from 36.7% to 76.7% across four runs in one afternoon of prompt work. Classification went from 80% to 93.3%. Errors went from one to zero. The line wobbled, jumped, and climbed, and every point on it told me something I could act on.&lt;/p&gt;

&lt;p&gt;That is what contact with reality looks like. A healthy metric breathes. So I have inverted my instincts. Steady numbers used to feel comforting and volatile ones used to feel alarming. Now the number that never moves is the one that keeps me up at night. I judge the autonomous operators I run the same way. When one of them turns in the same figure day after day, I do not ask what it is doing wrong. I ask what stopped being measured.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I believed a broken harness over my own work for as long as I had records. Zero felt like an answer, so I never read the grader. The fix was one auth token. The cost was every decision I made off that number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Grade the grader
&lt;/h2&gt;

&lt;p&gt;The rule I run now is simple. Before I trust an eval, I make it prove it can pass. Feed it a case where the right answer is known and pinned, and watch it score a success. A harness that has never emitted a passing grade is not strict. It is dead. And dead harnesses grade perfectly, forever.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The denial engine's eval now gates every ship at 90% against a golden set, and I trust that gate because I have watched it pass, fail, and change its mind as the system changed. It moves. That is how I know it is alive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Distrust any metric with no variance. Jagged is what real looks like.&lt;/li&gt;
&lt;li&gt;A flat zero is a flatline. Check the instrument's pulse before the patient's.&lt;/li&gt;
&lt;li&gt;Grade the grader. Run a known-good case through every eval and watch it pass.&lt;/li&gt;
&lt;li&gt;Never let a fallback swallow an error. One wrong token cost me months of belief.&lt;/li&gt;
&lt;li&gt;Perfectly round and perfectly stable deserve the same suspicion as perfectly bad.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The zero on my dashboard was never a grade. It was an instrument talking to itself. The numbers I trust now are the ones that move, because a number that moves is still touching the world. Most of us grade everything except the grader. Start there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/zero-is-not-a-score" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>advertising</category>
      <category>aiagentoperations</category>
      <category>developertooling</category>
      <category>evals</category>
    </item>
    <item>
      <title>Write Your Exceptions Down</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 23:50:17 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/write-your-exceptions-down-3c7g</link>
      <guid>https://dev.to/nabbilkhan/write-your-exceptions-down-3c7g</guid>
      <description>&lt;p&gt;I had a rule with no exceptions. RETSBAN, my primary agent, runs local inference only: open models on my own hardware, no cloud, no fallback. If the GPU box is down, the agent is down. Then Mia, the agent that runs my marketing, needed a frontier model to do her job well. And I did something that felt strangely formal for a one-person company. I amended my own policy, in writing, with the date attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  No one was in the room
&lt;/h2&gt;

&lt;p&gt;Here is what took me a while to see. A human employee remembers the day you made an exception. They were in the room when you said fine, just this once. An agent was never in the room. There is no room. Every session starts cold from the files, and the files are the only memory the company has.&lt;/p&gt;

&lt;p&gt;So when the policy says local only, no exceptions, and reality contains an exception, one of two things happens. Either the agent obeys the file and blocks work I actually want done, or it notices the contradiction and starts guessing which side to trust. The guessing is the dangerous case. A rule that has been contradicted once, silently, is not a rule anymore. Every agent that loads it gets to decide, in every session, whether to believe it. You never see the deciding. You just see the drift.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An exception that lives in your head does not bend a rule. It erases it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The amendment
&lt;/h2&gt;

&lt;p&gt;On 2026-04-23 I opened the policy file, a file literally named local_only_no_anthropic, and narrowed it instead of breaking it. The amendment names who is exempt: Mia, and only Mia. It says why: marketing work that needs capability the local stack does not have. It carries the date, and it points to the full model-topology doc for anyone, human or agent, who wants the whole picture. RETSBAN's constraint did not move an inch. Still local only. Still no fallback. Still down when the GPU box is down.&lt;/p&gt;

&lt;p&gt;That is the difference between an amendment and a repeal. An unwritten exception repeals the rule and hides the repeal. A written amendment narrows the rule and makes it stronger, because now the rule has visibly survived contact with a real exception. The next agent to read the file does not have to wonder if the local-only line is stale. It can see the line was tested, on a specific date, for a specific reason, and held.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; Before this I kept preferences in my head and re-explained them session after session. Every correction died with the session that heard it. I was giving the same review, over and over, to a workforce with perfect skill and no memory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same move in medical billing
&lt;/h2&gt;

&lt;p&gt;This is not an agent-fleet quirk. Last week, in my medical-billing business, I retired a ship gate on the denial-assessment engine. The old gate required the tuned API to agree with a weaker agent rater. A 150-case run showed the gate measured the wrong thing: on genuine divergences the API matched the golden answers about 78 percent of the time, and the agent rater about 38. The gate could never pass, and passing it would have meant nothing.&lt;/p&gt;

&lt;p&gt;I did not quietly stop running it. The change record says the gate was retired, says why, and says when the change was ratified: 2026-07-08. It says what replaced it: score the API against golden answers directly, with the floor at 90 percent. Whoever touches that system next quarter, human or agent, inherits the reasoning and not just the residue.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; cloud calls RETSBAN has made, before and after the amendment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; agent named in the exception&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;150&lt;/strong&gt; cases in the run that killed the old ship gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;90%&lt;/strong&gt; the floor the replacement gate has to clear&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Nearly three months after the amendment, both rules hold exactly as written. RETSBAN has made zero cloud calls, Mia has exactly the access the file grants, and no session has argued with either fact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your judgment is production config
&lt;/h2&gt;

&lt;p&gt;You version your code. You probably version your infrastructure. The strange part of running an AI agent workforce is that your own judgment joins the list. What you want, what you forbid, what you decided to allow anyway: the agents execute whatever the file says. So the file is the policy, and an edit to the file is a deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; An exception you keep in your head deletes the rule. Amend the file instead, with the date, the scope, and the reason attached.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So this is what I do now, and what I would tell anyone running agents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the rule where the agent reads, not where you think. A rule outside the files does not exist.&lt;/li&gt;
&lt;li&gt;Amend, do not repeal. Narrow the rule and keep it.&lt;/li&gt;
&lt;li&gt;Attach the date, the scope, and the reason. A future session cannot ask you follow-up questions.&lt;/li&gt;
&lt;li&gt;The moment you catch yourself carrying an exception in your head, the rule is already gone. Writing it down is the undo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A rule you never amended is not a rule. It is a rumor about a rule, and every session of every agent decides whether to believe it. Write your exceptions down. A rule is only as real as its history.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/write-your-exceptions-down" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>marketing</category>
      <category>policy</category>
      <category>ai</category>
    </item>
    <item>
      <title>Who Owns the Row</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 23:20:15 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/who-owns-the-row-3ld3</link>
      <guid>https://dev.to/nabbilkhan/who-owns-the-row-3ld3</guid>
      <description>&lt;p&gt;One of my systems runs a nightly job with two sets of manners. On 154 rows it overwrites whatever it finds and asks nobody. On 15 rows it cannot change a single field; the best it can do is file a request and wait. Same job, same feed, same table. The only difference is whether a human being ever touched the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two doors
&lt;/h2&gt;

&lt;p&gt;The system is a command center for a workforce development operation. The State of Illinois publishes a directory of workforce centers, and every night my sync pulls that feed and compares it against what I have. Most of those sites, 154 right now, are anonymous directory entries. Nobody on my side has ever opened one. For those, the feed writes straight through. New address, new phone, new hours, done. Arguing with the state about a row nobody looks at would be silly. The feed is the only party that knows anything about that row, so the feed decides.&lt;/p&gt;

&lt;p&gt;Fifteen sites are different. Those are partnered centers, rows people on my side have filled with staff notes and visit history. Someone drove out there. Someone wrote down which door to use and who to ask for. When the feed disagrees with one of those rows, the sync does not write. It queues the change in a table called WorkNetPendingChange and waits for a person to approve it or reject it.&lt;/p&gt;

&lt;p&gt;And nothing gets deleted, ever. A site that vanishes from the state's export just gets a missingSince date. Feeds hiccup. Exports get truncated. A date is reversible. A delete that cascades through visit history is not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;154&lt;/strong&gt; directory sites the feed overwrites freely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15&lt;/strong&gt; partnered centers where changes wait for review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; rows the sync is allowed to delete&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The row, not the feed
&lt;/h2&gt;

&lt;p&gt;The usual way to design a sync is to ask whether the feed is the source of truth. That question hides an assumption: that authority belongs to the feed. It does not. It belongs to the row.&lt;/p&gt;

&lt;p&gt;Think about what each side actually knows. The state knows what the state published. It knows nothing about the visit last Tuesday, or the note saying the listed phone number rings a fax machine. On a row nobody here has touched, the feed's knowledge is all the knowledge there is, so it should win every conflict. On a row people have worked on, the feed holds the smaller share, and letting it overwrite means destroying the larger one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Authority is not a property of the feed. It is a property of the row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Give a sync write authority row by row. It overwrites freely where no person has done any work, and it only gets to propose changes where someone has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; Every sync I have ever regretted failed the same way. An upstream export had a bad night, and the job faithfully copied the bad night over months of human work. The overwrite took milliseconds. Getting back what a coordinator knew about a site took weeks, when it happened at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same rule keeps showing up
&lt;/h2&gt;

&lt;p&gt;Once I had the rule, I started seeing it all over my stack. The same command center runs an inbox agent that triages a shared mailbox. Routine messages it handles by itself. Anything that touches a real relationship goes to a review queue. Two doors again.&lt;/p&gt;

&lt;p&gt;My publishing pipeline works the same way. Work-log entries publish to the enterprise timeline on their own, because nobody's judgment rides on a changelog line. Public essays, the ones that carry my name, wait for approval. And in a different industry entirely, the denial-assessment engine I run in medical billing acts directly on the mechanical cases and calls escalate_to_human on the few where judgment actually lives.&lt;/p&gt;

&lt;p&gt;Three corners of my work, one rule. Automation gets full authority over work no person has touched. The moment a person touches it, the automation drops from writer to proposer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The sync keeps 154 rows fresh with zero human effort, and every drift against the 15 partnered centers gets human eyes before it lands. Fresh and safe at the same time, without trading one for the other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to build
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. It is a WHERE clause and a pending-changes table. The hard part is deciding to build it, because "the feed is the source of truth" sounds so clean. If you are wiring an external feed into a system people actually work in, here is what I would do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give write authority per row, not per feed. Source of truth is a row-level question.&lt;/li&gt;
&lt;li&gt;Let human work demote the feed. The first staff note on a row turns the feed from a writer into an advisor.&lt;/li&gt;
&lt;li&gt;Never let a sync delete. Record the absence and let a person decide what it means.&lt;/li&gt;
&lt;li&gt;Make every change pick a door: direct write or review queue. No third path.&lt;/li&gt;
&lt;li&gt;Keep the queue short enough that people actually read it. A review queue nobody reads is just a delete with extra steps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Machines should own what nobody cares about, so people can own what they do. Go look at which rows in your database have fingerprints on them. Those are the ones the feed should have to ask about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/who-owns-the-row" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataauthority</category>
      <category>govtech</category>
      <category>workforcedevelopment</category>
      <category>ai</category>
    </item>
    <item>
      <title>Who Owns the Clock</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 22:50:14 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/who-owns-the-clock-51kh</link>
      <guid>https://dev.to/nabbilkhan/who-owns-the-clock-51kh</guid>
      <description>&lt;p&gt;A patient asked why she got two reminder texts a day when everyone else got one. Fair question. She had two open episodes in my Medicare RTM engine, and I had written the reminder logic to run once per open episode. Two episodes, two clocks, twice the nagging. The scheduler fired exactly on time. It always does. A few months later, in a different industry, I caught myself starting to write the same bug again. That was when I finally saw what the bug was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler is innocent
&lt;/h2&gt;

&lt;p&gt;When a cadence goes wrong, the scheduler is the first place everyone looks. I looked there too. I read the cron expression. I checked the timezone. I hunted for drift. Everything was fine, because cron is almost always fine. The scheduler is the most audited hundred lines in any system I run. The bug was somewhere I had never thought to look, because I had never noticed I was deciding anything there. I had let the episode own the clock.&lt;/p&gt;

&lt;p&gt;The fix was one sentence long: remind once per patient, not once per open episode. A patient does not experience episodes. She experiences her phone buzzing at dinner. The moment I said the fix out loud I could hear how obvious it was, and that is exactly why it survived review. It did not look like a decision. It looked like plumbing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; A patient with two open episodes got double the reminders because I let each episode own its own clock. The scheduler was innocent. The ownership was wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same bug in a different industry
&lt;/h2&gt;

&lt;p&gt;Then came workforce development. I built an outreach system where coaches call candidates about medical assistant training. Different industry, different codebase, not one shared line between them. The system has a cadence gate: how long a coach has to wait before touching the same candidate again. My first draft reset that clock on every logged attempt.&lt;/p&gt;

&lt;p&gt;Think about what that does. A coach leaves five voicemails in a week. Each one resets the clock, so the system decides the relationship is warm. It stops prompting follow-ups for someone nobody has actually talked to. Five voicemails, and the candidate sinks quietly in the queue, marked fresh by calls she never answered.&lt;/p&gt;

&lt;p&gt;This time I caught it before it shipped, because the RTM bug had taught me the question to ask. The fix was again one sentence: the gate resets only on one outcome, reached. Attempts write notes. They do not write time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A voicemail is not a relationship. It is you, talking to yourself, on the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Five logged voicemails now write five notes and move the clock zero seconds. One verb resets the gate: reached. The system stopped mistaking effort for contact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Nouns and verbs
&lt;/h2&gt;

&lt;p&gt;Healthcare and workforce development. Medicare billing rules on one side, career coaching on the other. Zero shared code, identical bug. Both times I went hunting in the scheduler, and both times the scheduler turned out to be a metronome doing its job. The defect lived in two words I had chosen without noticing I was choosing: which noun owns the clock, and which verb is allowed to reset it.&lt;/p&gt;

&lt;p&gt;In the RTM engine the noun was wrong. The episode owned the clock when the patient should have. In the outreach system the verb was wrong. "Tried" would have reset the clock when only "reached" should. That is the whole taxonomy. Every cadence bug I have shipped, in any industry, has been one of those two words.&lt;/p&gt;

&lt;p&gt;Why did neither bug look like a bug? Because neither decision looked like a decision. The noun hides in a foreign key. The verb hides in a WHERE clause. Review catches bad logic, and this was not bad logic. It was bad grammar, and the grammar compiled.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Every cadence bug lives in two words, not in the scheduler: which noun owns the clock, and which verb is allowed to reset it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; open episodes, one patient, twice the reminders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5&lt;/strong&gt; voicemails that moved the clock zero seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; verb allowed to reset the outreach gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; lines of code shared between the two systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;When I build anything with a clock in it, I write the two words down before I write the code. The noun goes at the top of the file. The verb goes next to it. Everything else is implementation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name the noun that owns the clock, and make it the noun a human experiences. Patients feel phones buzz. They do not feel episodes.&lt;/li&gt;
&lt;li&gt;List every verb allowed to reset the clock, then cut the list. Most clocks deserve exactly one verb.&lt;/li&gt;
&lt;li&gt;Let every other verb write a note instead of time. History is cheap. Resets are expensive.&lt;/li&gt;
&lt;li&gt;Test the plurals: two episodes, five attempts. The singular case always passes, which is why it proves nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scheduler was never the interesting part. The interesting part is the sentence you wrote without noticing you were writing it. So when a cadence goes wrong, do not read the cron expression first. Read your nouns and your verbs. And if you run systems in more than one industry and keep meeting the same small bug in different clothes, write it down and tell someone. The builders need to find each other.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/who-owns-the-clock" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>scheduling</category>
      <category>workforcedevelopment</category>
      <category>ai</category>
    </item>
    <item>
      <title>Watching or Working</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 22:20:12 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/watching-or-working-f0n</link>
      <guid>https://dev.to/nabbilkhan/watching-or-working-f0n</guid>
      <description>&lt;p&gt;Until this week, the console that files all my Medicare claims had no way to sign out. Plain forms, no animation, not one hour of visual design. Meanwhile the marketing site for the same product got choreographed scroll animations and hand-built React recreations of those very screens. The copies are prettier than the originals. That sounds like a scandal. I think the money went exactly where it should have, and most software points it the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the polish went
&lt;/h2&gt;

&lt;p&gt;clearpathcare.ai is the prettiest thing I have ever shipped. Every scroll position is choreographed with GSAP and Lenis. Every console screen was rebuilt by hand in React so it could slide in at just the right moment. Days of art direction for a page most people will see once.&lt;/p&gt;

&lt;p&gt;The console behind it, the tool that enrolls patients, logs the calls, and files the claims, got none of that. Zero hours of visual design. Not because it matters less. It matters more; it is the part that makes the money.&lt;/p&gt;

&lt;p&gt;The difference is what the person in front of each screen is doing. A visitor to the site is leaning back. They are judging, and they decide in about thirty seconds whether you are real. Looking good is the whole job. A biller in the console is leaning in. They are there all day, and every animation between them and the claim is friction. The best thing that screen can do is get out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The koi fish
&lt;/h2&gt;

&lt;p&gt;I know what happens when you get this wrong, because I got it wrong once. MOTOR is a quoting engine I built for construction contractors. At some point I gave the quoting flow a koi companion: a little animated fish that swam along while you built a quote. I thought it was charming.&lt;/p&gt;

&lt;p&gt;Contractors killed it inside three quotes. The feedback was not a design critique. It was "pretty annoying" and "just give me the quote."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; MOTOR's quoting flow got an animated koi fish to keep users company. Contractors killed it inside three quotes. The verdict was "pretty annoying" and "just give me the quote." Decoration inside a work tool is a tax you charge on every single use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just give me the quote. That sentence taught me more about design than any book. A worker does not experience your polish as a gift. They experience it as one more thing standing between them and being done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;So here is the rule I use now. Do not ask how important the system is. Ask what the person in front of it is doing. Watching? Spend on looks. Working? Spend on speed.&lt;/p&gt;

&lt;p&gt;By that rule, the console being the most important thing I own and the plainest is not a contradiction. It is the rule working.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Spend design where people watch and speed where people work. The budget follows what the user is doing, not how much the system matters.&lt;/p&gt;

&lt;p&gt;Polish is for people who are watching. Speed is for people who are working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This does not mean work tools get no design. This week the console finally got real design attention, and every bit of it was invisible: the browser back button works now, breadcrumbs show where you are, a confusing device field got a sane default and a plain hint. Nobody will screenshot any of it. Everyone who works in it will feel it.&lt;/p&gt;

&lt;p&gt;And when the console did need to be watched, because prospects wanted to see it run, I did not decorate the console. I bolted a guided demo onto the side: nine chapters, about forty steps, strictly read only. The show got its own stage. The tool stayed plain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; The console's guided demo shipped as nine chapters and roughly forty read-only steps. The show got its own surface, and the screens people actually work in stayed plain and fast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; quotes before contractors killed the koi fish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; hours of visual design in the console that files the claims&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9&lt;/strong&gt; chapters in the guided demo, a surface built for watching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;40&lt;/strong&gt; steps of polish kept out of the working screens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;I run systems in healthcare, in construction software, and in marketing, and the same one-line question now gates every design hour in all three: is the person on the other side of this screen watching or working?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask watching or working before you spend a single design hour.&lt;/li&gt;
&lt;li&gt;Spend looks on screens people judge and leave. Spend speed on screens people live in.&lt;/li&gt;
&lt;li&gt;Work tools still deserve design, the invisible kind: a back button that works, a sane default, a breadcrumb that tells you where you are.&lt;/li&gt;
&lt;li&gt;If a work tool needs to be watched, build the show a separate surface, like a demo, and keep it out of the flow.&lt;/li&gt;
&lt;li&gt;When a working user calls something pretty annoying, believe the annoying, not the pretty.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step back far enough and the pattern is simple: the closer a screen sits to the money, the plainer it gets. If the part of your product that does the real work looks embarrassingly plain, you are probably spending right. And if you run an ugly system that quietly earns, I would like to compare notes, because we are making the same bet. The money is made in plain rooms. The pretty rooms are how people find the door.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/watching-or-working" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>constructionsoftware</category>
      <category>design</category>
      <category>healthcare</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Underpowered Tests Lie</title>
      <dc:creator>Nabbil Khan</dc:creator>
      <pubDate>Sat, 18 Jul 2026 21:50:11 +0000</pubDate>
      <link>https://dev.to/nabbilkhan/underpowered-tests-lie-3cdl</link>
      <guid>https://dev.to/nabbilkhan/underpowered-tests-lie-3cdl</guid>
      <description>&lt;p&gt;Last month I audited the statistics inside Mia, my ad-testing agent, and found a 20x error that had never broken anything. No exception, no failed assertion, nothing in the logs. The textbook shortcut said each arm of an A/B test needed 1,111 users. The exact formula, run at the 1.9 percent click-through rate ads actually get, said 22,278. Every test Mia had sized was running on a twentieth of the data it needed, and every one of them still announced a winner. I run AI systems in several industries, and the failures that cost the most all have this shape: nothing breaks, and the answer is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the shortcut came from
&lt;/h2&gt;

&lt;p&gt;The shortcut is Lehr's equation: sixteen over the effect size squared. It is in the textbooks because it is easy, and it is easy because it was derived where the math is friendliest, near coin-flip rates, where variance peaks and everything is symmetric. Ads do not live there. Ads live at 1.9 percent, where the effects worth catching are tiny in absolute terms and the samples you need to see them are huge. The shortcut knows none of this. It returns 1,111 and moves on.&lt;/p&gt;

&lt;p&gt;The part that took me longest to accept is that the formula is not wrong. It works fine in the world it came from. The bug was mine. I carried it into a different world without re-deriving it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.9%&lt;/strong&gt; the base click-through rate Mia lives at&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,111&lt;/strong&gt; per-arm sample the textbook shortcut prescribed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22,278&lt;/strong&gt; per-arm sample the exact formula actually requires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14%&lt;/strong&gt; real false-positive rate of a three-arm test at raw p &amp;lt; 0.05&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What an underpowered test says
&lt;/h2&gt;

&lt;p&gt;You would think an underpowered test would fail. It cannot. There is no runtime check for statistical power. The test collects its too-small sample, computes a p-value, and hands down a verdict in the same confident voice it would use with twenty times the data. Sometimes the verdict is even right. That is what makes it dangerous. A crash gets fixed the same afternoon. A confident wrong answer gets acted on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An underpowered test never says it does not know. It says B wins, and it says it with a straight face.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The audit found a second lie. Mia judged three-arm tests at raw p below 0.05, which sounds like a 5 percent false-positive rate. With three comparisons it is closer to 14 percent. One test in seven was crowning a winner that did not exist. Holm-Bonferroni fixes that in a few lines of code. Writing the lines took minutes. Suspecting I needed them took an audit nobody asked for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; The 20x sizing error and the 14 percent false-positive rate sat in production together, and the code ran green the whole time. Every signal I could see said the system worked. The only thing wrong was the answers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same lie in healthcare
&lt;/h2&gt;

&lt;p&gt;I caught this in ads, but I only recognized it because I had already paid for it in healthcare. Daniel, my denial-assessment agent, is graded against a golden set of 30 cases. When a prompt change moved classification accuracy from 80 to 93.3 percent, I did not believe it until the eval said the same thing four runs in a row. Thirty cases is a small sample. A single run there can lie as smoothly as an underpowered ad test, and it lies the same way: not with an error, with a number.&lt;/p&gt;

&lt;p&gt;That is the cross-industry pattern. It has nothing to do with ads or insurance claims. It is about verdicts that sound the same whether or not they earned it. Ad tests, eval harnesses, dashboards: none of them has a voice for "not enough data." If you want that voice, you have to build it yourself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Mia now sizes every test with the exact unpooled formula at the observed base rate, refuses to declare a winner before that sample is met, and applies Holm-Bonferroni across arms. The fix took a day. Finding it took an audit nobody asked for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Re-derive at your base rate
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Every statistical rule of thumb is calibrated for someone else's base rate until you have re-derived it at yours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the rule I run on now, in every industry I touch. Here is what it looks like in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-derive the formula at your own base rate before you trust it. The textbook derived it at theirs.&lt;/li&gt;
&lt;li&gt;Ask what the system says when it does not know. If the answer sounds the same as when it does, you have a liar, not a tool.&lt;/li&gt;
&lt;li&gt;Count your comparisons. Three arms at raw p below 0.05 is a 14 percent false-positive rate, not 5.&lt;/li&gt;
&lt;li&gt;Audit the judge, not just the work. The most expensive bugs live in the thing doing the grading.&lt;/li&gt;
&lt;li&gt;Treat green as a timestamp, not a verdict. It means the code ran, not that the answer is right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tests that fail loudly are the cheap ones. You see them, you fix them, you move on. The expensive ones succeed quietly at the wrong thing, for months, while everyone trusts them a little more each week. If your experiments run at base rates the textbook writers never pictured, this is the week to re-derive your formulas. And if you catch one of these lies in your own stack, write it up. The operators who actually check are rare, and we should find each other. The math was never the hard part. The hard part is doubting a number that looks right.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nabbilkhan.com/posts/underpowered-tests-lie" rel="noopener noreferrer"&gt;nabbilkhan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>advertising</category>
      <category>marketinganalytics</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
