<?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: lamingsrb</title>
    <description>The latest articles on DEV Community by lamingsrb (@lamingsrb).</description>
    <link>https://dev.to/lamingsrb</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%2F3993279%2F31006581-315c-4581-89fb-4bd5e8bb0768.png</url>
      <title>DEV Community: lamingsrb</title>
      <link>https://dev.to/lamingsrb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lamingsrb"/>
    <language>en</language>
    <item>
      <title>LLM Evals in 2026: A Practitioner's Field Guide</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 09 Jul 2026 06:55:56 +0000</pubDate>
      <link>https://dev.to/lamingsrb/llm-evals-in-2026-a-practitioners-field-guide-26if</link>
      <guid>https://dev.to/lamingsrb/llm-evals-in-2026-a-practitioners-field-guide-26if</guid>
      <description>&lt;h1&gt;
  
  
  LLM Evals in 2026: A Practitioner's Field Guide
&lt;/h1&gt;

&lt;p&gt;Last week I watched a team ship a support agent that passed 94% of their eval suite and then hallucinated a refund policy on day two of production. Their eval was measuring the wrong thing, well. This guide is what I wish someone had handed me the first time I built an LLM eval harness that actually caught regressions before customers did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "LLM evals" actually means in 2026
&lt;/h2&gt;

&lt;p&gt;An LLM eval is a repeatable measurement of a model or agent's output quality on a fixed dataset, using deterministic checks, model-graded judges, or human review, run in CI or on a schedule so you can detect regressions before shipping. In 2026 the term covers four distinct things people conflate: &lt;strong&gt;offline benchmarks&lt;/strong&gt; (fixed dataset, run pre-merge), &lt;strong&gt;online evals&lt;/strong&gt; (production traces sampled and scored), &lt;strong&gt;guardrails&lt;/strong&gt; (runtime checks that block bad outputs), and &lt;strong&gt;judge calibration&lt;/strong&gt; (measuring whether your LLM-as-judge agrees with humans). If your team says "we have evals" and cannot tell you which of the four they mean, they do not have evals. They have a vibes-based test set.&lt;/p&gt;

&lt;p&gt;The mental model I use with clients: evals are unit tests for probabilistic systems. They will not be green/red. They will be distributions. Your job is to make the distribution shift visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eval framework landscape, honestly compared
&lt;/h2&gt;

&lt;p&gt;I have used all four of these on real projects in the last 18 months. Here is how I actually pick between them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Self-host&lt;/th&gt;
&lt;th&gt;LLM-judge built in&lt;/th&gt;
&lt;th&gt;Trace-linked&lt;/th&gt;
&lt;th&gt;Where it hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Braintrust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teams that want a polished UI and fast iteration on prompts&lt;/td&gt;
&lt;td&gt;No (SaaS)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Cost at scale, vendor lock on eval definitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LangSmith&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teams already deep in LangChain/LangGraph&lt;/td&gt;
&lt;td&gt;Limited (enterprise)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, tightly&lt;/td&gt;
&lt;td&gt;Framework gravity: hard to leave once your traces live there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Promptfoo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Config-as-code, CI-first, model comparison&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Weak (bring your own tracing)&lt;/td&gt;
&lt;td&gt;UI is basic, no real prod monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ragas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RAG-specific metrics (faithfulness, context precision)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Only useful if your problem is retrieval, judge metrics are noisy without calibration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My default stack for a new client engagement: &lt;strong&gt;Promptfoo in CI + a lightweight custom dashboard on Postgres for online evals&lt;/strong&gt;. I add Ragas only when the RAG faithfulness question is central. I recommend Braintrust when a non-engineering PM needs to run experiments without opening a PR. LangSmith is fine if you are already on LangChain and not planning to leave. I do not use it as a starting point in 2026 because the migration cost later is real.&lt;/p&gt;

&lt;p&gt;The single biggest mistake I see: teams pick the framework before they have written 20 real eval cases by hand. The framework is 10% of the problem. The dataset is 90%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12 failure modes I check for on every LLM system
&lt;/h2&gt;

&lt;p&gt;This is the table I keep in my head, and now on paper. Each row is a failure I have shipped or watched someone else ship in production. Each has a specific check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;th&gt;How I catch it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prompt drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small prompt edit tanks 3 unrelated cases&lt;/td&gt;
&lt;td&gt;Snapshot eval on every prompt PR, diff scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hallucinated citations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent invents URL, doc ID, or policy&lt;/td&gt;
&lt;td&gt;Regex check against known-good ID list + faithfulness judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tool-call malformation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON args missing required field&lt;/td&gt;
&lt;td&gt;Schema validation on every tool call, fail loud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Silent tool failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tool returns empty, agent proceeds as if it worked&lt;/td&gt;
&lt;td&gt;Assert non-empty tool return, log to trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Context poisoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bad retrieval chunk flips the answer&lt;/td&gt;
&lt;td&gt;Retrieval eval separate from generation eval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Judge bias&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LLM judge always prefers longer answers&lt;/td&gt;
&lt;td&gt;Calibrate judge against 50 human labels quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Refusal creep&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Model version upgrade adds unwanted refusals&lt;/td&gt;
&lt;td&gt;"Should answer" set: track refusal rate over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Loop / infinite planning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent replans same step forever&lt;/td&gt;
&lt;td&gt;Max-steps guardrail + step-count metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cost blowup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One query burns 400k tokens&lt;/td&gt;
&lt;td&gt;Per-trace token budget alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Latency regression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;p95 doubles after model swap&lt;/td&gt;
&lt;td&gt;Latency in the eval report, not just quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PII leakage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Model echoes user data into logs or downstream calls&lt;/td&gt;
&lt;td&gt;Regex + entity detector on outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Eval set rot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test cases become trivial after months of prompt tuning&lt;/td&gt;
&lt;td&gt;Rotate 10% of eval set per quarter with real prod failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you build a scorecard around these 12, you will catch roughly 80% of what actually breaks LLM systems in production. The remaining 20% is domain-specific, and that is where the interesting engineering happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I use to build an eval suite from scratch
&lt;/h2&gt;

&lt;p&gt;Every engagement follows the same rough sequence. It takes about a week for a scoped agent, three weeks for something ambitious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Collect 50 real inputs, not synthetic ones.&lt;/strong&gt; If the system is not live yet, sit with the people who will use it and ask them to type the messages they would actually send. Synthetic eval inputs generated by GPT are the reason so many production systems fail on real traffic. The distribution is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Hand-label expected behavior for each input.&lt;/strong&gt; Not expected output, expected behavior. "Should refuse", "should call &lt;code&gt;search_docs&lt;/code&gt; with query about pricing", "should ask a clarifying question". Behavior labels survive prompt changes. Exact-output labels do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Write deterministic checks first.&lt;/strong&gt; Anything you can catch with a regex, a JSON schema, or a substring match, catch that way. Deterministic checks are free, fast, and never disagree with themselves. I usually get 30-40% of my checks deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Add LLM-as-judge for the rest, then calibrate.&lt;/strong&gt; Write your judge prompt. Run it on 50 cases. Have a human (usually me and the domain expert) label the same 50. Compute agreement. If your judge disagrees with humans more than 15% of the time, the judge prompt is broken, not the model. I wrote about this in more depth in my post on Hamel's method vs a lean in-house setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Wire it into CI and a nightly cron.&lt;/strong&gt; CI blocks PRs that regress the eval score by more than 2 points on any critical metric. The nightly job runs against a sample of yesterday's production traces so you catch drift the fixed set will not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Publish the numbers where the team sees them.&lt;/strong&gt; A Slack post every morning with the score, a small table of what regressed, and a link to the diff. If the number lives in a dashboard nobody opens, you do not have evals. You have decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real numbers from a recent build
&lt;/h2&gt;

&lt;p&gt;For a content generation agent I shipped this year, the eval investment looked like this. I am sharing the actual numbers because "evals are worth it" without numbers is a slogan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to write initial 60-case eval suite: &lt;strong&gt;9 hours&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Time to calibrate the LLM judge to 91% human agreement: &lt;strong&gt;4 hours&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Model + judge cost per full eval run: &lt;strong&gt;$0.42&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CI overhead added per PR: &lt;strong&gt;~90 seconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Regressions caught before merge in first 8 weeks: &lt;strong&gt;11&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Estimated cost of those 11 regressions reaching prod (rework + reputation): I do not know, but the two that involved wrong pricing claims would have been bad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The eval suite paid for itself the first time it caught a prompt tweak that broke tool calling on 4 out of 60 cases. That took 90 seconds in CI. Debugging it in production would have taken hours and would have shipped wrong outputs to real users first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where LLM-as-judge quietly lies to you
&lt;/h2&gt;

&lt;p&gt;This is the section I wish someone had written for me two years ago. LLM judges have systematic biases and if you do not measure them, your eval score is a lie you are telling yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position bias.&lt;/strong&gt; In pairwise comparison ("is A or B better?"), most judges prefer whichever answer comes first. Fix: run every pair twice, swapped, and only count agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Length bias.&lt;/strong&gt; Judges prefer longer answers even when shorter is correct. Fix: include "concise" as an explicit criterion in the rubric, or normalize by length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-preference.&lt;/strong&gt; GPT-4 class judges prefer GPT-4 class outputs. Claude prefers Claude. If you use the same model family to generate and judge, you are grading your own homework. Fix: use a different family for the judge, or triangulate with two judges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rubric ambiguity.&lt;/strong&gt; "Rate helpfulness 1-5" produces noise. "Score 1 if the answer cites the correct policy section by ID, 0 otherwise" produces signal. Push rubrics toward binary or ternary decisions with explicit criteria whenever you can.&lt;/p&gt;

&lt;p&gt;I recalibrate judges every time I change the underlying model version. When Anthropic or OpenAI ships a new snapshot, judge behavior shifts. Skipping calibration and trusting the score is how teams end up with green dashboards and angry users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do if I were starting today
&lt;/h2&gt;

&lt;p&gt;If you are a founder or head of engineering standing up an LLM system in 2026, this is the shortest path I know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do not pick a framework yet.&lt;/strong&gt; Write 50 real eval cases in a spreadsheet, by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship Promptfoo in CI on week one.&lt;/strong&gt; It costs nothing, it works, and the config format is portable if you outgrow it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track the 12 failure modes above&lt;/strong&gt; as a checklist, not a philosophy. Tick them off one by one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibrate your judge quarterly&lt;/strong&gt; against fresh human labels. Put it on the calendar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate 10% of the eval set every quarter&lt;/strong&gt; with real production failures. Otherwise the set rots and your scores lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not spend more than 20% of your engineering budget on the eval framework itself.&lt;/strong&gt; The dataset and the judge rubric are the leverage points. The tooling is a commodity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The teams I see get this right treat evals the way good backend engineers treat observability: a first-class part of the system, built as the system is built, not bolted on after the first outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Evals in 2026 are not a checkbox. They are the difference between an LLM system you can operate with confidence and one you are constantly patching in response to angry users. If you are somewhere in that gap and want a second set of eyes on your eval strategy, or want help standing one up from scratch, I take a small number of these engagements each quarter: &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;. More posts on production LLM work live on the blog if you want to keep reading.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last updated: July 2026. Written by Lazar Milicevic, Senior Technical Engineer, 10+ years in AI automation, cloud architecture and B2B SaaS. Founder of BizFlowAI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llmevals</category>
      <category>llmasajudge</category>
      <category>ragevaluation</category>
      <category>llmevaluationframework</category>
    </item>
    <item>
      <title>How to Build AI Agents From Scratch: A Blueprint</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 09 Jul 2026 06:55:53 +0000</pubDate>
      <link>https://dev.to/lamingsrb/how-to-build-ai-agents-from-scratch-a-blueprint-1eo8</link>
      <guid>https://dev.to/lamingsrb/how-to-build-ai-agents-from-scratch-a-blueprint-1eo8</guid>
      <description>&lt;h1&gt;
  
  
  How to Build AI Agents From Scratch: A Blueprint
&lt;/h1&gt;

&lt;p&gt;Every framework I have used in production, I have eventually gutted. LangChain, CrewAI, even parts of LangGraph. Not because they are bad, but because when an agent fails at 3 a.m. and you are staring at a stack trace inside three layers of abstractions you did not write, you start thinking: this loop is maybe 200 lines. Why do I need 40 dependencies to run it?&lt;/p&gt;

&lt;p&gt;This is the architecture I actually use to ship autonomous multi-agent systems, the ones that publish content, hit APIs, and run unattended for weeks. No framework religion. Just the parts that survive real traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent loop is smaller than you think
&lt;/h2&gt;

&lt;p&gt;An agent is a &lt;code&gt;while&lt;/code&gt; loop around an LLM call that can invoke tools and decide when to stop. That is the whole thing. Everything else (memory, planning, reflection, multi-agent choreography) is a variation on that core.&lt;/p&gt;

&lt;p&gt;Here is the minimal loop I start every new agent from. Python, no framework, uses the Anthropic SDK but the shape is identical for OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tool_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;MaxStepsExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent did not finish in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is roughly 30 lines and it will run a real agent. Everything I am about to describe is layered on top of this without changing the shape of the loop.&lt;/p&gt;

&lt;p&gt;Two things I have learned to bake in from day one: &lt;strong&gt;a hard step cap&lt;/strong&gt;, and &lt;strong&gt;structured tool_result blocks with an &lt;code&gt;is_error&lt;/code&gt; flag&lt;/strong&gt;. Without the step cap, one bad prompt burns $40 in a night. Without the error flag, the model quietly hallucinates that failed calls succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool design is where agents actually live or die
&lt;/h2&gt;

&lt;p&gt;The model is only as smart as the tools you give it. In production, 80% of my debugging time is not prompt tuning, it is tool interface work. Ambiguous tool names, missing error messages, and untyped inputs cause more agent failures than any model weakness.&lt;/p&gt;

&lt;p&gt;The rules I follow, without exception:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every tool is a pure function with a typed schema.&lt;/strong&gt; No hidden state. If the tool needs context, pass it in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool names are verbs.&lt;/strong&gt; &lt;code&gt;search_docs&lt;/code&gt;, not &lt;code&gt;docs_util&lt;/code&gt;. The model prompts itself with the name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are returned, not raised.&lt;/strong&gt; Every tool returns &lt;code&gt;{"ok": bool, "data": ..., "error": ...}&lt;/code&gt;. The model can then react.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions include when NOT to use the tool.&lt;/strong&gt; This alone cut wrong-tool-selection rates by half in one of my content agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency where possible.&lt;/strong&gt; Retries are cheap when the tool is safe to call twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a tool description snippet I have shipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish_post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Publish a finalized post to the CMS. Only call this AFTER `validate_post` returns ok=true. Do NOT call this to preview or draft. This action is irreversible for 5 minutes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input_schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line, "irreversible for 5 minutes", changes model behavior meaningfully. Claude and GPT-4-class models actually respect it. They pause and often call &lt;code&gt;validate_post&lt;/code&gt; first, even when I have not explicitly told them to in the system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: three layers, not one blob
&lt;/h2&gt;

&lt;p&gt;The most common mistake I see in home-rolled agents is treating memory as "just append to the context window". That works for 5 turns. It falls apart at 50, and it becomes unaffordable at 500.&lt;/p&gt;

&lt;p&gt;I split memory into three layers, each with its own retention policy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Current task messages, tool calls&lt;/td&gt;
&lt;td&gt;Current run&lt;/td&gt;
&lt;td&gt;In-memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodic&lt;/td&gt;
&lt;td&gt;Past task summaries, outcomes&lt;/td&gt;
&lt;td&gt;30-90 days&lt;/td&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;Facts, entities, learned patterns&lt;/td&gt;
&lt;td&gt;Indefinite&lt;/td&gt;
&lt;td&gt;pgvector + Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In one of my recent posts I broke down how I cut a 3.26M token conversation down to 118K by moving from a naive append-everything design to this layered model. The key insight: &lt;strong&gt;the model does not need history, it needs the right slice of history&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For semantic memory I use pgvector with hybrid search (BM25 + vector, fused with RRF). Pure vector search misses exact-match queries ("what is the SKU for product X"), pure keyword misses semantic queries ("similar customer complaints"). Hybrid wins in every eval I have ever run.&lt;/p&gt;

&lt;p&gt;For working memory, I aggressively summarize. Every 10 turns, a small model (Haiku or GPT-4o-mini) compresses the last 10 messages into 200 tokens of "here is what happened and what state we are in". The full log stays in the database for debugging. The context stays lean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestration: single agent, then supervisor, then graph
&lt;/h2&gt;

&lt;p&gt;Do not start with a multi-agent system. I know it is the fashionable thing. Start with one agent and a lot of tools. Escalate only when you hit a real limit.&lt;/p&gt;

&lt;p&gt;Here is the progression I actually use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: Single agent, many tools.&lt;/strong&gt; Works up to about 15-20 tools before the model starts confusing them. Cheap, easy to debug, testable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: Supervisor plus workers.&lt;/strong&gt; A planning agent decides which specialist to call. Specialists are themselves single agents with narrower tool sets. This is how BizFlowAI ContentStudio is built: a research agent, a writing agent, an SEO agent, a publishing agent, orchestrated by a supervisor that owns the workflow state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: Graph / DAG.&lt;/strong&gt; Only when you have deterministic branching, parallel work, and cycles that would be wasteful to let an LLM figure out on its own. This is where LangGraph earns its keep, and honestly, if I am at this level, I sometimes reach for it rather than rolling my own state machine.&lt;/p&gt;

&lt;p&gt;The heuristic: &lt;strong&gt;if you can draw the workflow on a napkin and it does not need to change per input, the orchestration should be code, not an LLM decision&lt;/strong&gt;. Every decision you outsource to the model is latency, cost, and a failure mode. Reserve LLM decisions for genuinely ambiguous branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling is 40% of production agents
&lt;/h2&gt;

&lt;p&gt;The demo works. The production version has to survive rate limits, tool timeouts, malformed JSON, hallucinated tool names, infinite loops, context overflow, and the occasional model outage. In my experience, roughly 40% of the code in a production agent is failure handling, not "agent logic".&lt;/p&gt;

&lt;p&gt;The layers I always put in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry with backoff on transient errors.&lt;/strong&gt; Model APIs 529, tool network calls, everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output validation.&lt;/strong&gt; If a tool expects JSON, validate it against a Pydantic schema and hand the model back the validation error on failure. The model will fix it in one turn 90% of the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop detection.&lt;/strong&gt; I hash the last three tool calls plus arguments. If the same hash repeats, I inject a system message: "You just repeated the same action. Reconsider your approach." That alone kills most infinite loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget caps per run.&lt;/strong&gt; Max steps, max tokens, max wall clock, max dollar. Any breach halts the run and logs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability from day one.&lt;/strong&gt; Every step is written to a trace store with inputs, outputs, latency, cost. I use a lightweight Postgres schema for this, not a hosted tool, so I can query traces with SQL. When something breaks at scale, you need to be able to ask "show me every run where the SEO agent called publish_post before validate_post".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the pattern I use for tool retries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tool_safely&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;TransientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transient error after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; retries: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bad arguments: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Please correct and retry.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tool failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the model sees the error text. That is deliberate. The model is your last line of recovery, not a helpless victim of a stack trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll your own vs LangGraph vs CrewAI
&lt;/h2&gt;

&lt;p&gt;I get this question weekly. Here is the honest answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roll your own if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your agent has fewer than about 5 distinct roles&lt;/li&gt;
&lt;li&gt;You need tight control over cost, latency, and error surface&lt;/li&gt;
&lt;li&gt;You want to actually understand what your agent does when it misbehaves&lt;/li&gt;
&lt;li&gt;You are shipping to production and will maintain it for years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use LangGraph if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have genuine graph structure: parallel branches, cycles, checkpointing across a long-running workflow&lt;/li&gt;
&lt;li&gt;You want first-class human-in-the-loop primitives without building them&lt;/li&gt;
&lt;li&gt;Your team already knows the LangChain ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use CrewAI if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are prototyping a multi-agent concept quickly and the exact wiring does not matter yet&lt;/li&gt;
&lt;li&gt;Honestly, I have not shipped anything on CrewAI to production. It is fine for demos. I have always ended up rewriting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff nobody talks about: frameworks lock in a mental model. Once your team thinks in "Crews and Tasks" or "StateGraph nodes", it is very hard to think outside that model when you need to. Rolling your own means the mental model is: functions, state, loops. Universal.&lt;/p&gt;

&lt;p&gt;For reference, the 200-line loop I showed at the top has been the base of every production agent I have shipped in the last two years, including the content pipeline that publishes across multiple sites unattended. I have never regretted starting small.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do if starting a new agent today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the tools first.&lt;/strong&gt; Type them, test them without an LLM. If your tools are broken, no model saves you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with the 30-line loop.&lt;/strong&gt; Get one end-to-end run working with a real task before you add anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the trace store.&lt;/strong&gt; Postgres table, columns for run_id, step, role, content, tokens, cost, latency. Query with SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add budget caps.&lt;/strong&gt; Steps, tokens, dollars. Non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add loop detection and structured error returns.&lt;/strong&gt; These two catch 80% of production weirdness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then&lt;/strong&gt;, if the task actually needs it, split into supervisor and workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then&lt;/strong&gt;, if you have real graph structure, consider a framework.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The teams that ship fast are the ones that resist the urge to adopt heavy abstractions before they have felt the pain the abstraction solves. Every abstraction you adopt without earning it becomes a debugging tax later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Production agents are not a framework problem. They are a systems problem: tool design, memory layering, orchestration discipline, and relentless failure handling. Get those right and a 200-line loop will outperform a 10,000-line framework wrapper you do not understand.&lt;/p&gt;

&lt;p&gt;If you are building an agent that has to actually run in production and you want a second pair of eyes on the architecture, get in touch at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;. Or read more of what I have shipped and learned on the &lt;a href="https://lazar-milicevic.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>howtobuildaiagents</category>
      <category>aiagentarchitecture</category>
      <category>multiagentsystems</category>
      <category>agentlooppython</category>
    </item>
    <item>
      <title>WebMCP Runs In Chrome. My 400 Daily Tool Calls Don't.</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 09 Jul 2026 06:11:50 +0000</pubDate>
      <link>https://dev.to/lamingsrb/webmcp-runs-in-chrome-my-400-daily-tool-calls-dont-4ek3</link>
      <guid>https://dev.to/lamingsrb/webmcp-runs-in-chrome-my-400-daily-tool-calls-dont-4ek3</guid>
      <description>&lt;h1&gt;
  
  
  WebMCP Runs In Chrome. My 400 Daily Tool Calls Don't.
&lt;/h1&gt;

&lt;p&gt;Google I/O 2026 shipped WebMCP and half the AI Twitter timeline is calling it "the new MCP standard." It isn't. It's a browser-scoped protocol that solves a completely different problem than the MCP servers currently running on your VPS at 3 AM. Here's the boundary Google buried in the docs, and how to decide which side of it your agent belongs on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WebMCP actually is (and isn't)
&lt;/h2&gt;

&lt;p&gt;WebMCP is a browser-scoped tool protocol. It exposes tools to an agent &lt;strong&gt;from inside a Chrome tab&lt;/strong&gt; — the tools live in the page, auth is the user's active session, and the runtime is the browser itself. That's the entire surface area.&lt;/p&gt;

&lt;p&gt;When Google says "agentic web," they mean an agent that operates inside a tab the user already has open, using the cookies and OAuth tokens already loaded. That's a legitimate and useful pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Booking flows&lt;/strong&gt; — agent fills a multi-step form on a site the user is signed into&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards&lt;/strong&gt; — agent pulls a chart, exports it, drops it into a doc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-app copilots&lt;/strong&gt; — SaaS product ships tools its own users' agent can call&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Form fillers and page-scoped assistants&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What WebMCP is &lt;strong&gt;not&lt;/strong&gt;: a replacement for the stdio and HTTP MCP servers running headless on your machine or VPS. Different runtime, different auth model, different lifecycle. Calling it "the new MCP" is like calling a service worker "the new backend." Same protocol family, entirely different deployment target.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split that actually matters
&lt;/h2&gt;

&lt;p&gt;There's exactly one question you need to answer to pick correctly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Is a human looking at a screen when the agent runs?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes → WebMCP is on the table.&lt;br&gt;
If no → you need a real server-side MCP.&lt;/p&gt;

&lt;p&gt;That's it. Everything else is retweet noise.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;WebMCP&lt;/th&gt;
&lt;th&gt;stdio / HTTP MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Chrome tab&lt;/td&gt;
&lt;td&gt;Your process (local, VPS, container)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;User's browser session&lt;/td&gt;
&lt;td&gt;Your API keys / OAuth tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;User action in the page&lt;/td&gt;
&lt;td&gt;cron, webhook, queue, schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;While tab is open&lt;/td&gt;
&lt;td&gt;24/7 headless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credentials scope&lt;/td&gt;
&lt;td&gt;Whatever the user is logged into&lt;/td&gt;
&lt;td&gt;Whatever you gave the process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-account&lt;/td&gt;
&lt;td&gt;Painful (one browser session)&lt;/td&gt;
&lt;td&gt;Trivial (one process per tenant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs at 6 AM while you sleep&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I run three MCP servers in production. Gmail triage, Telegram messaging, invoicing. They sit on a WSL Ubuntu box, run headless as systemd services, and between them handle &lt;strong&gt;over 400 tool calls a day&lt;/strong&gt;. Zero of those calls involve a browser. There's no user session. There's no tab. The agent wakes on cron or a webhook, pulls email, decides what matters, drafts replies, pushes a Telegram notification, generates an invoice, goes back to sleep.&lt;/p&gt;

&lt;p&gt;WebMCP can't do any of that. Not because it's broken — because it's scoped to a runtime where a human is present.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a real server-side MCP looks like
&lt;/h2&gt;

&lt;p&gt;Here's the shape of one of my production servers, stripped to the bones. This is the Gmail triage server that runs on a 5-minute cron and processes the inbox before I'm awake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gmail_triage_server.py — stdio MCP server, runs as systemd service
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.stdio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stdio_server&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2.credentials&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail-triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Credentials loaded from disk once at startup.
# No browser, no user session — a service account / refresh token.
&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_authorized_user_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/agents/gmail.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_unread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is:unread -category:promotions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxResults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_results&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="nd"&gt;@app.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draft_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...creates a draft, does not send
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;stdio_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent that calls this doesn't care what browser you use. It doesn't even know a browser exists. The credentials live in a file with &lt;code&gt;chmod 600&lt;/code&gt;, the process runs as a dedicated user, and the tool calls are logged to a local SQLite file so I can audit what happened overnight.&lt;/p&gt;

&lt;p&gt;The systemd unit is boring on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/gmail-triage.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Gmail Triage MCP Server&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /opt/agents/gmail_triage_server.py&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare that to WebMCP. A WebMCP tool is declared by the page, discovered by the agent through the browser, and executed against the user's session. Rough shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Publisher-side: a page exposing a WebMCP tool --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add_to_cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add a SKU to the current cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// user's session cookie&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different world. That tool only exists while the tab is open, only works for the logged-in user, and only fires when an agent-capable browser decides to invoke it. Try to run that on a cron at 6 AM — you can't. There's no tab, no session, no user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where founders and operators keep getting this wrong
&lt;/h2&gt;

&lt;p&gt;The pattern I see repeatedly with SMB owners chasing every I/O announcement: they conflate "AI in the browser" with "AI running my business." They're not the same thing, and the difference costs real money when you build the wrong one.&lt;/p&gt;

&lt;p&gt;Concrete examples of workloads that &lt;strong&gt;need&lt;/strong&gt; a server-side MCP, not WebMCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inbox triage at 6 AM&lt;/strong&gt; — no one is awake, no browser is open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoice chase on the 1st of the month&lt;/strong&gt; — cron-driven, needs your Stripe/QuickBooks keys, not the user's session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead follow-up on a 48-hour delay&lt;/strong&gt; — queue-driven, runs whether you're on a call or on a flight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-tool sync&lt;/strong&gt; (CRM → email → Slack) — happens on webhooks, not on tab loads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports pushed to Telegram/Slack&lt;/strong&gt; while you're in a meeting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant automations&lt;/strong&gt; where you serve dozens of clients — one browser session cannot be dozens of users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those have a "user looking at a screen" moment. All of them need a process holding &lt;em&gt;your&lt;/em&gt; credentials, not a user's session cookie.&lt;/p&gt;

&lt;p&gt;The inverse mistake is also real: building a heavy server-side agent to do something that's genuinely browser-scoped, like helping a user complete a checkout on a site they're already signed into. That's where WebMCP is the right answer and a VPS is overkill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick rule of thumb
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Agent triggered by a &lt;strong&gt;human click in a specific tab&lt;/strong&gt; → WebMCP&lt;/li&gt;
&lt;li&gt;Agent triggered by &lt;strong&gt;time, event, queue, or webhook&lt;/strong&gt; → stdio/HTTP MCP on your infra&lt;/li&gt;
&lt;li&gt;Agent needs &lt;strong&gt;your&lt;/strong&gt; API keys → server-side, always&lt;/li&gt;
&lt;li&gt;Agent needs the &lt;strong&gt;user's&lt;/strong&gt; logged-in session → WebMCP, always&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The other two I/O 2026 updates, quickly
&lt;/h2&gt;

&lt;p&gt;Since everyone's asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built-in AI in Chrome (Gemini Nano exposed to web pages)&lt;/strong&gt; — useful for tiny client-side tasks. Summarize this form, classify this input, redact PII before it leaves the browser. It's cheap because it runs on the user's device. It's limited for the same reason: small model, no tool use, no persistence, no cross-session memory. Use it for UX polish. Do not build a business process on it. If your "AI feature" breaks the moment the user closes the tab, it isn't a business process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills&lt;/strong&gt; — reusable capability bundles an agent can load on demand. This one is actually interesting for solopreneurs because it maps cleanly onto the problem of "my prompt library is now 40 markdown files and I can't remember which one does what." Collapsing those into shippable, versioned skill units is a real pattern. I'll write that one up separately — it deserves its own post, not a paragraph.&lt;/p&gt;

&lt;p&gt;For context, the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol spec&lt;/a&gt; itself still defines stdio and HTTP as the transport surfaces for server-side MCP. WebMCP sits alongside it as a browser transport, not on top of it. Read the actual spec before believing a hot take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;The server-side MCP work I described above — Gmail triage, invoicing, Telegram/Slack notifications, lead follow-up, cross-tool sync — is exactly what we wire up for clients at &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;bizflowai.io&lt;/a&gt; every week. The build is boring on purpose: real MCP servers on a VPS or your own box, holding your credentials, driven by cron and webhooks, logging every tool call so you can audit what the agent did overnight. Nothing fancy, nothing browser-dependent, nothing that stops working when you close a tab. If your automation needs to run while you're asleep, this is the shape it takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick based on where the agent runs, not what got retweeted
&lt;/h2&gt;

&lt;p&gt;WebMCP is real, useful, and correctly scoped for browser-resident tools. It is not a replacement for the MCP servers doing the actual work in your business. If your agent needs a human staring at a screen to function, WebMCP is on the table. If it needs to run at 6 AM, on the 1st of the month, or when a webhook fires — you need a server-side MCP holding your credentials, and no keynote is going to change that.&lt;/p&gt;

&lt;p&gt;400 tool calls a day. Zero browsers. Pick the runtime that matches the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>mcpservers</category>
      <category>agenticweb</category>
      <category>browserautomationagents</category>
    </item>
    <item>
      <title>Anthropic Shipped @Claude For Slack. My Team Runs On</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 09 Jul 2026 06:11:46 +0000</pubDate>
      <link>https://dev.to/lamingsrb/anthropic-shipped-claude-for-slack-my-team-runs-on-341h</link>
      <guid>https://dev.to/lamingsrb/anthropic-shipped-claude-for-slack-my-team-runs-on-341h</guid>
      <description>&lt;h1&gt;
  
  
  Anthropic Shipped &lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; for Slack. My Team Runs on Telegram.
&lt;/h1&gt;

&lt;p&gt;Anthropic just shipped &lt;code&gt;@Claude&lt;/code&gt; inside Slack channels. Tag the bot, it reads the thread, does work async, posts back. Nice product. Except roughly 95% of small businesses don't live in Slack — they run on WhatsApp, Telegram, and Gmail. If you're a solopreneur or a 1-to-10-person team, here's the exact four-part recipe I use to run the same pattern in Telegram for under $12/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Anthropic actually shipped (and who it's for)
&lt;/h2&gt;

&lt;p&gt;Anthropic shipped an enterprise distribution deal wearing a product launch t-shirt. &lt;code&gt;@Claude&lt;/code&gt; for Slack lets you tag the bot in a channel or thread, gives it channel memory, connects to your other apps, and returns work asynchronously — but only on Slack Team and Enterprise plans. That's the punchline: it lives where the annual contracts live.&lt;/p&gt;

&lt;p&gt;Look at the raw user counts. Slack's own reporting puts it around 35–40 million weekly active users globally. WhatsApp is over 2 billion. Telegram is over 900 million. Gmail sits around 1.8 billion. In the 1-to-10-employee segment outside US tech, Slack penetration is single digits. Small teams in Europe, LATAM, and most of Asia coordinate in WhatsApp groups and run pipeline out of Gmail. They are not about to add Slack seats at $15/user/month just to get an &lt;code&gt;@Claude&lt;/code&gt; mention.&lt;/p&gt;

&lt;p&gt;That's a rational call for Anthropic — Slack is where the enterprise procurement motion already exists. It's just not a product for the operator segment. And the pattern they productized is trivially replicable on any messenger with a bot API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Weekly/monthly active users&lt;/th&gt;
&lt;th&gt;Bot API&lt;/th&gt;
&lt;th&gt;Cost to run a mention-bot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;~35–40M WAU&lt;/td&gt;
&lt;td&gt;Yes, paid plan&lt;/td&gt;
&lt;td&gt;$15/user/mo + API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram&lt;/td&gt;
&lt;td&gt;~900M MAU&lt;/td&gt;
&lt;td&gt;Yes, free&lt;/td&gt;
&lt;td&gt;~$5–12/mo API only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp Business&lt;/td&gt;
&lt;td&gt;~2B MAU&lt;/td&gt;
&lt;td&gt;Yes, metered&lt;/td&gt;
&lt;td&gt;$0.005–0.08/conversation + API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail&lt;/td&gt;
&lt;td&gt;~1.8B MAU&lt;/td&gt;
&lt;td&gt;Pub/Sub push&lt;/td&gt;
&lt;td&gt;Free tier + API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The four-part recipe (works in any messenger)
&lt;/h2&gt;

&lt;p&gt;Every mention-bot is the same four moving parts: a &lt;strong&gt;webhook&lt;/strong&gt; that fires on mention, a &lt;strong&gt;context store&lt;/strong&gt; that holds recent thread history, a &lt;strong&gt;model call&lt;/strong&gt; that produces the reply, and a &lt;strong&gt;send&lt;/strong&gt; back to the same thread. Total code for a working Telegram version: three files, ~200 lines, running on a $10 VPS or a home server.&lt;/p&gt;

&lt;p&gt;Here's the shape, framework-free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[messenger webhook]  →  [ingest handler]  →  [SQLite context store]
                                              ↓
                                        [Claude API call]
                                              ↓
                                    [messenger send endpoint]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No Zapier, no n8n, no vendor lock-in. Every piece is a documented public API.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four parts, one line each
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhook&lt;/strong&gt;: Telegram/WhatsApp/Gmail fires a POST when your bot is mentioned or a filter matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt;: SQLite table keyed by &lt;code&gt;thread_id&lt;/code&gt;, last 20 messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt;: Claude API with a system prompt describing the bot's job + thread context as the user turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply&lt;/strong&gt;: Same messenger's &lt;code&gt;sendMessage&lt;/code&gt; endpoint, back to the same thread.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 1 + 2: Webhook and SQLite context store
&lt;/h2&gt;

&lt;p&gt;The first 80% of the work is capturing the trigger and storing context. Do this step alone this week — don't wire Claude yet. Prove you can catch every mention and log the thread. It costs zero dollars.&lt;/p&gt;

&lt;p&gt;Register a Telegram bot with &lt;code&gt;@BotFather&lt;/code&gt;, add it to a group, set a webhook URL pointing at your server, and enable group message access. The webhook payload arrives as JSON on every message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# webhook.py — FastAPI ingest, ~40 lines
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS msgs(
        chat_id INTEGER, msg_id INTEGER, user TEXT,
        text TEXT, ts INTEGER, mentions_bot INTEGER)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;

&lt;span class="n"&gt;BOT_USERNAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BOT_USERNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. "myops_bot"
&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;chat_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mentions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BOT_USERNAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO msgs VALUES (?,?,?,?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
         &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# enqueue for the Claude worker — covered in Part 3
&lt;/span&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship that, add the bot to one real group, and let it log for two days. You'll immediately see which threads matter and how much context 20 messages actually covers. On my ops group, 20 messages is roughly the last 45 minutes of conversation — usually enough for the bot to answer without asking clarifying questions.&lt;/p&gt;

&lt;p&gt;For WhatsApp, swap the webhook for the Meta Cloud API endpoint and the same table works. For Gmail, use a Gmail push notification via Pub/Sub with a label filter — the "thread" is the email thread ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Claude API call, wired to thread context
&lt;/h2&gt;

&lt;p&gt;When a mention fires, pull the last N messages from SQLite for that chat, wrap them as one user turn, and call the API. That's the entire agent. Memory is not magic — it's a &lt;code&gt;SELECT ... ORDER BY ts DESC LIMIT 20&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# worker.py — the Claude call
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are the ops assistant for a 4-person consulting team.
Answer briefly. If the group is discussing a client, pull the relevant
client facts from the thread. Never invent numbers. If unsure, ask.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT user, text FROM msgs WHERE chat_id=? ORDER BY ts DESC LIMIT 20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recent thread:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Respond to the latest @mention.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real numbers from my server last month across three groups (roughly 40 mentions/day):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input tokens billed: ~2.1M&lt;/li&gt;
&lt;li&gt;Output tokens billed: ~180K&lt;/li&gt;
&lt;li&gt;Total Anthropic bill: &lt;strong&gt;$11.42&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VPS: home server, $0 marginal&lt;/li&gt;
&lt;li&gt;Slack seats saved for a 4-person team at $15/user/mo: &lt;strong&gt;$720/year&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole economic argument. The Slack version isn't better — it's just packaged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: Reply to the same thread + reliability tricks
&lt;/h2&gt;

&lt;p&gt;Sending back to Telegram is one HTTP POST. The trick is quoting the original message so the reply lands in-thread and doesn't spam the channel, and rate-limiting so a chatty group doesn't burn your API budget on one bad afternoon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;TG_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TG_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TG_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sendMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply_to_message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reply_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse_mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three cheap reliability moves that have saved me real money and real embarrassment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debounce&lt;/strong&gt;: if two mentions fire within 10 seconds in the same chat, collapse to one call. Group chats double-tap constantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily cap&lt;/strong&gt;: hard-stop each chat at 200 mentions/day. One runaway loop between two bots almost cost me $80 in a single evening before I added this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-quiet&lt;/strong&gt;: if the Claude call errors, log it, don't reply. Silent failure is better than "I'm sorry, I encountered an error" flooding the group.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For WhatsApp Business the send endpoint is a POST to &lt;code&gt;graph.facebook.com/v20.0/{phone_id}/messages&lt;/code&gt; with a &lt;code&gt;context.message_id&lt;/code&gt; for the reply target. For Gmail, &lt;code&gt;users.messages.send&lt;/code&gt; with the original &lt;code&gt;threadId&lt;/code&gt; keeps replies in-thread. Same four parts, different vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the moat is the wiring, not the model
&lt;/h2&gt;

&lt;p&gt;Every frontier lab is going to ship the polished version of this workflow for the platform where their enterprise buyers already pay. Anthropic picked Slack. OpenAI's team offering is heading the same direction. Neither of them is going to ship a first-class WhatsApp Business or Telegram bot because there's no procurement contract at the other end.&lt;/p&gt;

&lt;p&gt;That leaves a specific opening for operators. The API is public. The messenger webhooks are public. The only thing that isn't handed to you is knowledge of your own workflow — which client threads matter, which questions repeat, which context the bot needs to be useful instead of annoying. That's not a technical moat. It's an operator moat, and it takes about a weekend to build the first version.&lt;/p&gt;

&lt;p&gt;The one practical takeaway for this week: pick the messenger your team actually uses, register a bot, and set up the webhook + SQLite logger. Don't add Claude yet. Just prove you can capture every mention and the surrounding thread. Once that's stable, adding the API call is an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where bizflowai.io fits in
&lt;/h2&gt;

&lt;p&gt;This is exactly the pattern we assemble for clients at &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;bizflowai.io&lt;/a&gt; — mention-bots and inbox agents wired into the messenger the team already lives in (Telegram, WhatsApp Business, Gmail), backed by a small SQLite or Postgres context store, running on a $10 VPS or the client's own server with no per-seat fees. Same four parts as above, hardened with the debouncing, daily caps, and fail-quiet handling that keep the API bill boring. It's not a SaaS subscription and it's not a Slack replacement — it's the wiring most small teams don't have time to write themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>claudeslackintegration</category>
      <category>whatsappbotapi</category>
      <category>telegrambotautomation</category>
      <category>aiforsmallbusiness</category>
    </item>
    <item>
      <title>Claude Fable 5 Global Access Restored: Enterprise Playbook</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:11:37 +0000</pubDate>
      <link>https://dev.to/lamingsrb/claude-fable-5-global-access-restored-enterprise-playbook-2l3d</link>
      <guid>https://dev.to/lamingsrb/claude-fable-5-global-access-restored-enterprise-playbook-2l3d</guid>
      <description>&lt;h1&gt;
  
  
  Claude Fable 5 Global Access Restored: Enterprise Playbook
&lt;/h1&gt;

&lt;p&gt;Your Claude-based agent pipelines went dark on June 12 when Anthropic pulled global access to Fable 5 overnight. As of today, access is back — but re-enabling production agents isn't a toggle flip. If you ship automation for a living, here's what changed, what didn't, and how to bring your workloads back online without breaking SLAs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened: Export Controls Withdrawn, Access Restored
&lt;/h2&gt;

&lt;p&gt;The U.S. Department of Commerce issued emergency export controls on June 12, 2026, that classified Claude Fable 5 — and its cybersecurity counterpart — under technology export restrictions. Anthropic's immediate response was to suspend all global access outside the U.S. to both models rather than risk compliance violations. Last night, Commerce withdrew those controls, and Anthropic began restoring global availability today.&lt;/p&gt;

&lt;p&gt;For engineering teams, the timeline looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;June 12, 2026&lt;/td&gt;
&lt;td&gt;Export control order issued&lt;/td&gt;
&lt;td&gt;Anthropic suspends Fable 5 globally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June 12 – July 4&lt;/td&gt;
&lt;td&gt;Appeals, negotiations, industry pressure&lt;/td&gt;
&lt;td&gt;Workloads either downgraded or offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 4, 2026&lt;/td&gt;
&lt;td&gt;Commerce withdraws controls&lt;/td&gt;
&lt;td&gt;Legal barrier removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 5, 2026&lt;/td&gt;
&lt;td&gt;Anthropic restores global access&lt;/td&gt;
&lt;td&gt;Pipelines can be re-enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The withdrawal means enterprises outside the U.S. can resume using Fable 5 through the standard channels: Anthropic's API, Amazon Bedrock, and Google Cloud's Vertex AI Model Garden. There is no separate "international" tier — the same model endpoints serve all regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Enterprises Can Access Claude Fable 5 Right Now
&lt;/h2&gt;

&lt;p&gt;Fable 5 is available through three primary enterprise channels. The right choice depends on your existing cloud commitments, compliance requirements, and whether you need features like guardrails, provisioning, or private endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic API (direct)&lt;/strong&gt; — The fastest path if you already have an Anthropic console account. You get the latest model versions first, direct access to features like extended thinking and tool use, and Anthropic's native rate limits. Best for teams that want the shortest dependency chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Bedrock&lt;/strong&gt; — If your infrastructure lives in AWS, Bedrock gives you Fable 5 with IAM-based access control, CloudWatch logging, and VPC integration. You trade some feature lag (Bedrock typically lags the direct API by days to weeks on new capabilities) for enterprise governance. Check the Bedrock model catalog page for current regional availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Cloud Vertex AI&lt;/strong&gt; — Similar proposition to Bedrock but for GCP shops. You get Fable 5 alongside Google's Model Garden tooling, IAM, and audit logging. If you're already running workloads on Vertex, enabling Fable 5 is a configuration change, not an architecture change.&lt;/p&gt;

&lt;p&gt;A practical note: if your pipelines were running on Bedrock or Vertex before June 12, your model access may already be restored. AWS and GCP typically re-enable models at the platform level. If you were on the direct Anthropic API and your workspace region was set to a non-U.S. locale, you may need to verify that your organization's geo settings haven't been permanently altered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assessing Pipeline Damage From the 23-Day Outage
&lt;/h2&gt;

&lt;p&gt;Before flipping models back on, take 30 minutes to assess what actually broke during the outage. Rushing a rollback is how you introduce silent failures — a model that technically responds but behaves differently enough to corrupt downstream data.&lt;/p&gt;

&lt;p&gt;Start by checking three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Check which pipelines fell back to a downgrade model&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"claude-fable"&lt;/span&gt; ./pipeline-logs/ &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'select(.model != "claude-fable-5") | .pipeline_id, .timestamp, .model'&lt;/span&gt;

&lt;span class="c"&gt;# 2. Find jobs that failed outright (no fallback configured)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"model_not_available&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;access_denied"&lt;/span&gt; ./pipeline-logs/ &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.pipeline_id, .error.message'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Identify any cached responses that may be stale or wrong&lt;/span&gt;
find ./cache/ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*fable*"&lt;/span&gt; &lt;span class="nt"&gt;-newer&lt;/span&gt; ./config/suspension-timestamp.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The teams that weathered the outage best had one thing in common: they'd built model-agnostic abstractions. Their agent logic referenced a model alias (&lt;code&gt;primary-reasoning-model&lt;/code&gt;) rather than a hardcoded model string. When Fable 5 went down, they changed one environment variable and rerouted to Claude Opus or Sonnet. The teams that suffered hardest had &lt;code&gt;"model": "claude-fable-5"&lt;/code&gt; hardcoded into 40 different API calls across their codebase.&lt;/p&gt;

&lt;p&gt;If you're in the second group, this outage is your signal to fix that. It won't be the last time a model becomes temporarily unavailable — whether due to export controls, capacity issues, or deprecation cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safely Upgrading Your Agent Pipelines Back to Fable 5
&lt;/h2&gt;

&lt;p&gt;Restoring Fable 5 access doesn't mean you should point every agent at it and ship. Model behavior can shift between the version you were running on June 11 and the one coming back online today. Anthropic has not confirmed whether the restored model is the exact same checkpoint or a subsequent build.&lt;/p&gt;

&lt;p&gt;Here's a rollout approach that minimizes risk:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Smoke test (1-2 hours)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run your standard evaluation suite against the restored model. If you don't have a standard eval suite, run 20-50 of your most representative production inputs through the model and compare outputs to your last-known-good baseline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Load your eval cases — real production inputs with known-good outputs
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evals/fable5_baseline.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;eval_cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-fable-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# verify exact model ID in Anthropic's docs
&lt;/span&gt;        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Compare against baseline — flag any drift &amp;gt; 15%
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_cases&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_semantic_distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠ REVIEW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Case &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;case_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: drift=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 2: Canary deployment (24-48 hours)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Route 5-10% of traffic to Fable 5. Monitor error rates, latency, and — most importantly — output quality. A model can return 200 OK responses that are subtly worse. If you have human reviewers in your loop, have them blind-score Fable 5 outputs against the fallback model you've been running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Full rollout&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the canary shows no regression, ramp to 100%. Keep your fallback model configured and ready for at least a week. Export controls were withdrawn once; they could be reimposed. Treat model availability as ephemeral.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Model Architecture: The Real Lesson
&lt;/h2&gt;

&lt;p&gt;The Fable 5 outage exposed a structural weakness in how many teams build agent pipelines: single-model dependency. When one model becomes unavailable, everything stops. The fix isn't to avoid Fable 5 — it's to build routing layers that treat models as interchangeable infrastructure.&lt;/p&gt;

&lt;p&gt;A basic multi-model router looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRIMARY_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-fable-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FALLBACK_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tertiary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TERTIARY_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;complexity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Route based on task requirements and model availability.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_is_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;complexity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tertiary&lt;/span&gt;

        &lt;span class="c1"&gt;# Route expensive reasoning to primary only when warranted
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_generation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complex_analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;complexity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fallback&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Check a health endpoint or cache recent failures
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_health_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point isn't the specific code — it's the principle. Your agent logic should never assume a specific model exists. Define your pipeline in terms of capabilities ("fast reasoning," "long-context analysis," "code generation") and let a router map those to whatever model is available and cost-effective at runtime.&lt;/p&gt;

&lt;p&gt;This architecture also saves money. Not every task needs Fable 5. Simple classification, formatting, and extraction tasks run fine on smaller, cheaper models. Routing intelligently can cut API costs significantly without degrading output quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance and Data Governance Going Forward
&lt;/h2&gt;

&lt;p&gt;The export control episode will have lasting effects on how enterprises evaluate AI model dependencies. Legal and procurement teams are now asking questions that engineering teams haven't traditionally prepared for.&lt;/p&gt;

&lt;p&gt;If you're operating in regulated industries — finance, healthcare, government contractors — document the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data residency&lt;/strong&gt;: Where does your inference traffic actually get processed? Anthropic's API may route requests through different regions depending on load. If you're on Bedrock or Vertex, requests stay within your configured cloud region. For data-sensitive workloads, pin your region explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit trails&lt;/strong&gt;: Can you reconstruct which model processed a given input at a given time? If a regulator asks "was this decision made using an AI model subject to export controls on June 15?" you need a precise answer. Log the model ID, version, and timestamp for every inference call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vendor lock-in exposure&lt;/strong&gt;: How quickly could you switch models if Fable 5 became permanently unavailable in your jurisdiction? If the answer is "we'd need to rewrite our entire agent layer," you have a concentration risk. Insurance companies and compliance auditors are starting to ask this question directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contractual fallbacks&lt;/strong&gt;: If you have an enterprise agreement with Anthropic, AWS, or Google, review what it says about model availability disruptions. Most standard contracts don't guarantee specific model availability. If you need that guarantee, it's a negotiation point.&lt;/p&gt;

&lt;p&gt;For U.S.-based teams, the export control withdrawal is mostly good news — access is restored, move on. For teams in the EU, UK, Canada, Australia, and other jurisdictions, the 23-day outage was a stress test. The regulators noticed. Expect forthcoming guidance from the EU AI Office and UK AI Safety Institute on model dependency risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-Enabling Cybersecurity Workloads
&lt;/h2&gt;

&lt;p&gt;Fable 5's cybersecurity counterpart — the less restricted variant mentioned in the original export order — was also caught in the suspension. If you were running security-focused agent workloads (threat intelligence parsing, vulnerability analysis, security log triage), those need the same careful re-enablement.&lt;/p&gt;

&lt;p&gt;Cybersecurity models carry additional considerations: they may be subject to separate usage policies, require enhanced verification, or have different logging requirements depending on your sector. Before pointing a security agent back at the restored model, verify that your usage patterns still comply with Anthropic's usage policies — those may have been updated during the 23-day window.&lt;/p&gt;

&lt;p&gt;If you're building security automation on Fable 5's cybersecurity variant, test against your threat intelligence corpus first. The model's behavior on adversarial inputs, prompt injection attempts, and malformed security data should be verified before production traffic resumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BizFlowAI Approaches This
&lt;/h2&gt;

&lt;p&gt;We've spent the last several days helping clients reroute and restore Claude-based agent pipelines after the Fable 5 suspension. The pattern that works — and that we build into every engagement — is model-agnostic agent architecture with automatic fallback. When Fable 5 went dark on June 12, our clients' pipelines degraded gracefully to alternative models rather than crashing. Now that access is restored, we're running canary evaluations before flipping traffic back.&lt;/p&gt;

&lt;p&gt;If your pipelines broke hard during the outage and you want to build something that survives the next disruption — whether it's export controls, capacity issues, or model deprecations — book a discovery call. We'll look at your current architecture and map out where the single-point-of-failure risks are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Work with BizFlowAI
&lt;/h2&gt;

&lt;p&gt;If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://calendly.com/lamingsrb" rel="noopener noreferrer"&gt;Book a free discovery call&lt;/a&gt;&lt;/strong&gt; — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.&lt;/p&gt;

&lt;p&gt;More guides like this on the &lt;a href="https://dev.to/blog"&gt;BizFlowAI blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudefable5access</category>
      <category>anthropicapienterprise</category>
      <category>aimodelexportcontrols</category>
      <category>claudefable5bedrock</category>
    </item>
    <item>
      <title>3 Businesses, 1 Server, 7AM: The Operations Loop Nobody</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:11:34 +0000</pubDate>
      <link>https://dev.to/lamingsrb/3-businesses-1-server-7am-the-operations-loop-nobody-amh</link>
      <guid>https://dev.to/lamingsrb/3-businesses-1-server-7am-the-operations-loop-nobody-amh</guid>
      <description>&lt;h1&gt;
  
  
  3 Businesses, 1 Server, 7AM: The Operations Loop Nobody Teaches
&lt;/h1&gt;

&lt;p&gt;At 7AM every morning, my phone buzzes once. That single Telegram message tells me whether my three live systems survived the night — or if something broke at 3AM and I need to fix it before a client notices. Nobody teaches you this part. Courses show you how to ship a prototype in an afternoon. They never show you what happens on day 47 when an API times out and a client's invoice queue stalls.&lt;/p&gt;

&lt;p&gt;I run three automated systems on one home server — a Gmail-Telegram bot ecosystem, an invoicing automation tool, and a lead-gen engine. Real users pay for them daily. Here's exactly how I built the operations loop that keeps all three alive without hiring anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Morning Check: One Telegram Message Instead of 45 Minutes
&lt;/h2&gt;

&lt;p&gt;The core insight is this: a solopreneur doesn't need dashboards. You need a single, scannable summary that tells you "all green" or "here's the red." If it's green, you go back to sleep. If it's red, you know exactly where to look.&lt;/p&gt;

&lt;p&gt;Every morning at 7AM, my operations bot sends a Telegram message with three sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 MORNING REPORT — 2026-07-05

✅ Gmail Ecosystem
   Processed: 47 emails (3 accounts)
   Flagged: 4 urgent
   Errors: 0

✅ Invoice System
   Generated: 12 invoices
   Queued for approval: 3
   Errors: 0

🔴 Lead-Gen Engine
   Outreach sent: 89
   Replies received: 7
   Errors: 1 (5:30AM — SMTP timeout, retried x3, escalated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That red block tells me everything I need: which system, what time, what error, and what the retry logic already tried. In this case, the SMTP timeout resolved itself after retry — but I still know it happened.&lt;/p&gt;

&lt;p&gt;The alternative most solopreneurs default to is opening 6 browser tabs, checking each tool's dashboard, manually cross-referencing timestamps, and trying to remember what "normal" looks like. That's 45 minutes gone before you've done any actual work.&lt;/p&gt;

&lt;p&gt;The message itself is generated by a Python script that runs at 7AM sharp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_morning_report&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pulls status from all 3 systems and sends one Telegram message.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gmail_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_gmail_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;midnight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;invoice_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_invoice_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;midnight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;leadgen_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_leadgen_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;midnight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;format_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gmail Ecosystem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gmail_stats&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;format_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice System&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice_stats&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;format_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lead-Gen Engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leadgen_stats&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ All systems nominal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gmail_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leadgen_stats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔴 Issues detected — see below&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📊 MORNING REPORT — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;send_telegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Grafana. No Datadog. A text message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Three Systems, One Home Server, Zero Cloud Bills
&lt;/h2&gt;

&lt;p&gt;All three systems run on a single PC-PC home server using WSL Ubuntu on consumer hardware. No cloud instances. No Kubernetes. No AWS bill that makes you nervous. The entire stack costs the electricity to run a desktop PC — roughly $15-20/month depending on your local rate.&lt;/p&gt;

&lt;p&gt;The three systems are completely decoupled, each running as an independent process with its own cron schedule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Dependencies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gmail-Telegram bot&lt;/td&gt;
&lt;td&gt;Every 15 min&lt;/td&gt;
&lt;td&gt;Triage, categorize, forward&lt;/td&gt;
&lt;td&gt;Gmail API, Telegram API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invoice system&lt;/td&gt;
&lt;td&gt;Every 2 hours&lt;/td&gt;
&lt;td&gt;Pull billing emails, generate invoices&lt;/td&gt;
&lt;td&gt;Gmail API, PDF library, SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lead-gen engine&lt;/td&gt;
&lt;td&gt;3x daily (8AM, 1PM, 5:30PM)&lt;/td&gt;
&lt;td&gt;Outreach, qualification&lt;/td&gt;
&lt;td&gt;SMTP, LinkedIn API, PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decoupling is the critical part. If the Gmail bot crashes at 2AM, the invoice system and lead-gen engine keep running. They don't share a process, they don't share a database, and they don't share a failure mode. Here's the process structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Each system runs as an independent systemd service&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-units | &lt;span class="nb"&gt;grep &lt;/span&gt;active | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"bizflowai.io|gmail|leadgen"&lt;/span&gt;
gmail-bot.service        active running
invoice-engine.service   active running
leadgen-engine.service   active running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service has its own working directory, its own virtualenv, and its own log files. If one needs to restart, the others don't even notice. This is the poor-person's microservices architecture — and for a one-person business, it's exactly right.&lt;/p&gt;

&lt;p&gt;The directory structure on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/lazar/
├── systems/
│   ├── gmail-ecosystem/
│   │   ├── venv/
│   │   ├── src/
│   │   ├── config/
│   │   └── logs/
│   ├── invoice-system/
│   │   ├── venv/
│   │   ├── src/
│   │   ├── config/
│   │   └── logs/
│   └── leadgen-engine/
│       ├── venv/
│       ├── src/
│       ├── config/
│       └── logs/
├── shared/
│   ├── telegram-bot.py     # Shared alerting
│   └── error_handler.py    # Shared retry logic
└── deploy.sh               # Single deploy script
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;shared/&lt;/code&gt; directory contains the Telegram bot and error handling logic that all three systems import. Everything else is isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Monitoring Layer: Three Message Types, Not Three Dashboards
&lt;/h2&gt;

&lt;p&gt;Most one-person operations fall apart at the monitoring layer because solopreneurs build like enterprise teams — dashboards, metrics, alerts routed to email. Then they stop checking the dashboard because it's always green, and miss the one time it goes red.&lt;/p&gt;

&lt;p&gt;My monitoring layer is three Telegram message types. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message 1: Morning summary&lt;/strong&gt; (described above) — daily at 7AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message 2: Immediate failure alert&lt;/strong&gt; — within 60 seconds of any unhandled error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send immediate Telegram alert with stack trace.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;🔴 FAILURE — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Stack: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_exc&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Retries attempted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Action needed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Auto-retry queued&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;will_retry&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Manual fix required&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;send_telegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Message 3: Weekly trend report&lt;/strong&gt; — every Sunday at 6PM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📈 WEEKLY TRENDS — Week of Jun 28

Email volume: 312 (+8% vs last week)
Invoice errors: 2 (stable)
Lead-gen conversion: 4.1% (down from 5.2% — investigate)

Top failure mode: SMTP timeout (3 occurrences)
Avg resolution time: 11 min
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That weekly report is where I catch slow degradation before it becomes an emergency. If email volume creeps up 8% per week for three weeks, I know I'm going to hit a rate limit soon and can adjust preemptively.&lt;/p&gt;

&lt;p&gt;The point isn't the specific messages — it's that I can check the state of my entire infrastructure from my phone, whether I'm at my desk or walking outside. Three message types. No browser tabs. No "I should really set up Grafana someday."&lt;/p&gt;

&lt;h2&gt;
  
  
  When Things Break at 3AM: A Real Failure Walkthrough
&lt;/h2&gt;

&lt;p&gt;Here's a real failure from last Tuesday at 3:12AM. The Gmail API started returning 429 rate limit errors because one of the managed accounts received an unusually large batch of forwarded emails. Here's exactly what happened, step by step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3:12:04 AM&lt;/strong&gt; — Gmail API returns HTTP 429 on batch processing request.&lt;br&gt;
&lt;strong&gt;3:12:05 AM&lt;/strong&gt; — Error handler catches the 429, logs it, starts exponential backoff retry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_gmail_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Gmail API call with exponential backoff for 429 errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;  &lt;span class="c1"&gt;# seconds
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request_fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HttpError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 30, 60, 120
&lt;/span&gt;                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;429 rate limit, retry in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s (attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="c1"&gt;# All retries exhausted — escalate
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alert_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gmail Ecosystem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;will_retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3:12:35 AM&lt;/strong&gt; — First retry (30s delay). Still 429.&lt;br&gt;
&lt;strong&gt;3:13:35 AM&lt;/strong&gt; — Second retry (60s delay). Still 429.&lt;br&gt;
&lt;strong&gt;3:15:35 AM&lt;/strong&gt; — Third retry (120s delay). Success on 6 of 8 pending emails.&lt;br&gt;
&lt;strong&gt;3:15:40 AM&lt;/strong&gt; — Two emails remain unprocessed. Telegram alert sent to my phone.&lt;/p&gt;

&lt;p&gt;When I woke up at 7AM, the morning report showed the alert from 3:15AM. Six of eight emails were handled automatically. Two needed manual intervention — one had an attachment format the parser didn't recognize, and one was flagged as suspicious by the spam filter but wasn't actually spam.&lt;/p&gt;

&lt;p&gt;Here's the fix loop. I opened Claude Code in the project directory, pasted in the error log, and asked it to diagnose. Claude read the error, looked at the retry logic in the codebase, and identified that the rate limit threshold was set too aggressively — processing 10 messages per second when the API allows 20 per minute. Claude wrote the fix, updated the config, and I reviewed the diff before committing. Total time: 8 minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What the diff looked like&lt;/span&gt;
- RATE_LIMIT_REQUESTS &lt;span class="o"&gt;=&lt;/span&gt; 10  &lt;span class="c"&gt;# per second&lt;/span&gt;
- RATE_LIMIT_WINDOW &lt;span class="o"&gt;=&lt;/span&gt; 1     &lt;span class="c"&gt;# seconds&lt;/span&gt;
+ RATE_LIMIT_REQUESTS &lt;span class="o"&gt;=&lt;/span&gt; 18  &lt;span class="c"&gt;# per minute (safe margin under 20)&lt;/span&gt;
+ RATE_LIMIT_WINDOW &lt;span class="o"&gt;=&lt;/span&gt; 60    &lt;span class="c"&gt;# seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I pushed to the repo, the deployment script pulled the update on the server, the service restarted, and the two stuck emails processed within 90 seconds. Zero downtime for the other two systems because they're independent processes.&lt;/p&gt;

&lt;p&gt;This is the loop that keeps a one-person business alive: build, monitor, fail, fix, deploy, repeat. The fix-deploy cycle needs to be fast because you don't have a team. Claude Code is what makes 8-minute fixes possible. I'm not writing code at 7AM with coffee in one hand — I'm reviewing code that Claude wrote based on a real error log. That's the difference between a system you can operate alone and a system that needs a developer on retainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying New Features Without Taking Systems Down
&lt;/h2&gt;

&lt;p&gt;The deployment workflow is the part where most solo-built systems die — not because the code is wrong, but because deploying changes feels risky, so you stop making changes, and the system rots. Here's how I keep the cycle fast.&lt;/p&gt;

&lt;p&gt;I never push directly to the branch running on the server. The git workflow is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Feature branch&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; fix/gmail-rate-limit

&lt;span class="c"&gt;# 2. Claude Code writes the fix&lt;/span&gt;
claude &lt;span class="s2"&gt;"Update rate limit config based on this error log: [paste]"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Review the diff&lt;/span&gt;
git diff

&lt;span class="c"&gt;# 4. Test locally against sample input&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_gmail_rate_limit.py

&lt;span class="c"&gt;# 5. Merge and deploy&lt;/span&gt;
git checkout main &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git merge fix/gmail-rate-limit
git push origin main
&lt;span class="c"&gt;# Deployment script handles the rest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment script on the server does three things: pulls the latest commit, runs a health check, and restarts the service. If the health check fails, it rolls back automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# deploy.sh — runs on server after git push&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;
&lt;span class="nv"&gt;HEALTH_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8000/health"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deploying &lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

&lt;span class="c"&gt;# Save current commit for rollback&lt;/span&gt;
&lt;span class="nv"&gt;PREV_COMMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Pull latest&lt;/span&gt;
git fetch origin
git checkout &lt;span class="nv"&gt;$BRANCH&lt;/span&gt;
git pull origin &lt;span class="nv"&gt;$BRANCH&lt;/span&gt;

&lt;span class="c"&gt;# Restart service&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;

&lt;span class="c"&gt;# Health check — 30 second window&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..6&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5
  &lt;span class="k"&gt;if &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nv"&gt;$HEALTH_URL&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Health check passed. Deployed &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# Health check failed — rollback&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🔴 Health check failed. Rolling back to &lt;/span&gt;&lt;span class="nv"&gt;$PREV_COMMIT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git checkout &lt;span class="nv"&gt;$PREV_COMMIT&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart &lt;span class="nv"&gt;$SERVICE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Rolled back. Manual investigation needed."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total time from feature request to production: about 12-18 minutes for a straightforward change. The auto-rollback has saved me twice — once when a dependency update broke the PDF library in the invoice system, and once when I accidentally tightened a regex too aggressively in the email parser.&lt;/p&gt;

&lt;p&gt;The key practices that make this safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never deploy on Friday afternoon (classic rule, still true)&lt;/li&gt;
&lt;li&gt;Always test locally against a sample input before merging&lt;/li&gt;
&lt;li&gt;If the health check catches it, the rollback handles it — you haven't broken anything for users&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How bizflowai.io Fits Into This Operations Loop
&lt;/h2&gt;

&lt;p&gt;Running three live systems solo taught me what breaks at 2AM and what actually matters at 7AM. The operations loop — the monitoring layer, the retry logic, the morning summary, the deploy-with-rollback — is what I folded into the tooling at bizflowai.io. The platform handles the repetitive parts: invoice generation pipelines, email triage with retry built in, lead-gen scheduling with health checks. You bring your Gmail accounts and your business rules; the retry logic, the Telegram alerts, and the deploy workflow are already wired. Less time building plumbing means more time actually fixing the 3AM problems when they matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>homeserverautomation</category>
      <category>telegrambotmonitoring</category>
      <category>gmailautomationbot</category>
      <category>smallbusinessinvoicingautomati</category>
    </item>
    <item>
      <title>How to Create an AI Agent: A Production Walkthrough</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:24:35 +0000</pubDate>
      <link>https://dev.to/lamingsrb/how-to-create-an-ai-agent-a-production-walkthrough-41ga</link>
      <guid>https://dev.to/lamingsrb/how-to-create-an-ai-agent-a-production-walkthrough-41ga</guid>
      <description>&lt;h1&gt;
  
  
  How to Create an AI Agent: A Production Walkthrough
&lt;/h1&gt;

&lt;p&gt;The first agent I shipped to production failed at 3am on a Sunday. It looped on a tool call, burned through $40 in tokens before my budget alarm fired, and left a half-written draft in the database with no way to resume. That night taught me more about agent design than any framework tutorial. Since then I have built a pattern I trust enough to leave running unattended for weeks at BizFlowAI, where agents research, write, optimize and publish content without me touching them.&lt;/p&gt;

&lt;p&gt;This is that pattern, stripped down to what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the job spec, not the framework
&lt;/h2&gt;

&lt;p&gt;Before you pick LangGraph, CrewAI, or roll your own, write the agent's job spec like you would for a junior engineer. One paragraph. What it owns, what it must never do, what "done" looks like, and which signals tell you it failed.&lt;/p&gt;

&lt;p&gt;Here is the spec for one of my production agents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Topic Researcher owns generating a ranked list of 20 content topics per site per week. It reads from &lt;code&gt;keyword_pool&lt;/code&gt; and &lt;code&gt;search_console_perf&lt;/code&gt;, writes to &lt;code&gt;topic_queue&lt;/code&gt;. It must never publish, never call paid APIs more than 8 times per run, and must finish in under 6 minutes. Done = 20 topics with score &amp;gt;= 0.6 and zero duplicates against the last 90 days. Failure signal = empty queue after a run, or any topic flagged by the dedupe check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you cannot write this paragraph, do not build the agent. You will end up with a "do everything" prompt that hallucinates its way through ambiguous tasks. The job spec becomes your evaluation rubric later, so write it carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb I use&lt;/strong&gt;: if the spec needs more than 5 tools or more than 3 decision branches, it is two agents, not one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the tools before you write the prompt
&lt;/h2&gt;

&lt;p&gt;Most agent failures I have debugged were not prompt failures. They were tool failures. The model called a tool with wrong arguments, the tool returned a 4MB JSON blob, or two tools had overlapping responsibilities and the model picked the wrong one.&lt;/p&gt;

&lt;p&gt;Treat tools like a public API you are shipping to a difficult customer. The customer is the LLM.&lt;/p&gt;

&lt;p&gt;Three rules I follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Each tool does one thing and returns a small, structured result.&lt;/strong&gt; If &lt;code&gt;search_database&lt;/code&gt; returns 200 rows, the model will choke or pick poorly. Return 10 with a &lt;code&gt;has_more&lt;/code&gt; flag and a &lt;code&gt;next_cursor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool names and parameter names are the prompt.&lt;/strong&gt; A tool called &lt;code&gt;fetch_recent_topics(days: int, min_score: float)&lt;/code&gt; is self-explanatory. A tool called &lt;code&gt;get_data(query: str)&lt;/code&gt; is a coin flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tool has idempotency keys and a dry-run mode.&lt;/strong&gt; When the agent retries after a timeout, you do not want duplicate publishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the actual signature I use for a publishing tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;site_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;draft_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# hash of draft_id + content_hash
&lt;/span&gt;    &lt;span class="n"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PublishResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Publishes a draft to the target CMS. 
    Returns PublishResult with url, published_at, and cms_post_id.
    If idempotency_key was used in the last 24h, returns the original result.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idempotency key has saved me at least four times. EventBridge retries, Lambda cold-start timeouts, network blips: all of them caused duplicate execution attempts in production. Without the key I would have shipped duplicate content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write a prompt that survives contact with reality
&lt;/h2&gt;

&lt;p&gt;I no longer write monolithic system prompts. I write a system prompt that is mostly constraints and a runtime context block that gets rebuilt every turn. The split matters because the system prompt is the contract and the context is the working memory.&lt;/p&gt;

&lt;p&gt;My template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;SYSTEM PROMPT (stable, ~600 tokens):
&lt;span class="p"&gt;-&lt;/span&gt; Role and goal (3 sentences max)
&lt;span class="p"&gt;-&lt;/span&gt; Hard constraints ("never call publish_post without dry_run first on first attempt")
&lt;span class="p"&gt;-&lt;/span&gt; Tool inventory with one-line guidance per tool
&lt;span class="p"&gt;-&lt;/span&gt; Output format for the final answer (JSON schema)
&lt;span class="p"&gt;-&lt;/span&gt; Stop conditions ("when topic_queue has 20 entries, call finalize and stop")

RUNTIME CONTEXT (rebuilt per turn):
&lt;span class="p"&gt;-&lt;/span&gt; Current task ID and attempt number
&lt;span class="p"&gt;-&lt;/span&gt; Tool call history compressed: last 3 calls in full, older ones summarized
&lt;span class="p"&gt;-&lt;/span&gt; Relevant memory entries pulled from pgvector (top 5 by relevance)
&lt;span class="p"&gt;-&lt;/span&gt; Budget left: tokens, tool calls, seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two specific things I have learned the hard way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell the agent its budget.&lt;/strong&gt; When I added "you have 8 tool calls remaining and 4 minutes" to the runtime context, my average run cost dropped roughly 30%. Models are surprisingly good at rationing when they know the limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make stop conditions explicit and machine-checkable.&lt;/strong&gt; "Stop when the task is complete" is not a stop condition. "Stop when &lt;code&gt;topic_queue&lt;/code&gt; count returned by &lt;code&gt;count_queue()&lt;/code&gt; is &amp;gt;= 20" is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: short, long, and the part everyone gets wrong
&lt;/h2&gt;

&lt;p&gt;Agents need three kinds of memory and most tutorials only cover one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Memory type&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;th&gt;Where I put it&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scratchpad&lt;/td&gt;
&lt;td&gt;Current turn's reasoning, tool results&lt;/td&gt;
&lt;td&gt;In-context, compressed each turn&lt;/td&gt;
&lt;td&gt;Single run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodic&lt;/td&gt;
&lt;td&gt;What happened in past runs (decisions, outcomes)&lt;/td&gt;
&lt;td&gt;Postgres table, summarized&lt;/td&gt;
&lt;td&gt;30-90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;Facts the agent should "know" (brand voice, prior topics)&lt;/td&gt;
&lt;td&gt;pgvector + BM25 hybrid (RRF)&lt;/td&gt;
&lt;td&gt;Indefinite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The part everyone gets wrong is episodic memory. Without it, your agent makes the same mistake every Tuesday. With it, you can write rules like "before generating a topic, check if a similar topic failed evaluation in the last 60 days, and if so, vary the angle."&lt;/p&gt;

&lt;p&gt;For semantic memory I use Postgres with pgvector and a BM25 index, then combine results with Reciprocal Rank Fusion. Pure vector search consistently missed exact-match keywords ("Q3 pricing" returned posts about Q1). RRF is 30 lines of SQL and fixes it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Simplified RRF combining vector and BM25&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rnk&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;site_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;bm&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rnk&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;site_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rnk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The control loop: the part that decides if you sleep at night
&lt;/h2&gt;

&lt;p&gt;A naive agent loop is &lt;code&gt;while not done: llm_call(); execute_tool()&lt;/code&gt;. That is how you get a 3am token explosion. Mine looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tool_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_or_init_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_tool_calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_tool_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_tool_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge_call&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nf"&gt;persist_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# so we can resume on crash
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five things in there that matter more than they look:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;persist_state&lt;/code&gt; every turn.&lt;/strong&gt; When Lambda times out at 15 minutes, the next invocation resumes from the last persisted turn. No work lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;policy.allows&lt;/code&gt;&lt;/strong&gt; is a separate function, not a prompt instruction. Hard rules ("never publish before 9am UTC", "never call paid APIs if budget &amp;lt; $0.20") live in code, not in English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;handoff&lt;/code&gt;&lt;/strong&gt; is a real state, not a failure. When the budget runs out, the agent writes its current state and the reason to a queue. A human or another agent picks it up. Agents that cannot ask for help are agents that fail silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency tied to &lt;code&gt;run_id&lt;/code&gt;.&lt;/strong&gt; Replays do not double-execute side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;build_context&lt;/code&gt; charges into the budget too.&lt;/strong&gt; Big context windows are deceptively expensive. I cap retrieved memory at 5 entries and summarize old tool calls aggressively.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Evaluation: the part that turns a demo into a product
&lt;/h2&gt;

&lt;p&gt;If you cannot tell me your agent's success rate on a fixed eval set, you do not have a production agent. You have a demo that has not failed yet.&lt;/p&gt;

&lt;p&gt;I run three evaluation layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Unit-level.&lt;/strong&gt; Each tool has tests with golden inputs and outputs. Boring, fast, runs on every commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Trajectory eval.&lt;/strong&gt; A frozen set of 30-50 tasks with expected outcomes. Run the full agent, score the trajectory and the final output. For scoring I use a mix: deterministic checks where possible (did the topic queue have 20 entries? was the schema valid?) and an LLM judge for subjective parts (was the topic on brand?). The LLM judge runs with a calibrated rubric and I spot-check 10% of its scores against my own judgment monthly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Production telemetry.&lt;/strong&gt; Every run logs: tool calls made, tokens used, wall time, budget exhausted, handoff reason, and a sample of final outputs. I look at the dashboard every Monday. Drift shows up in tool call counts before it shows up in output quality.&lt;/p&gt;

&lt;p&gt;A real number from my own systems: when I added trajectory eval to the content agent and started gating deploys on it, my "weird output" rate dropped from roughly 1 in 25 runs to under 1 in 200. Not zero. Never zero. But low enough to leave running unattended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment: serverless, scheduled, and boring
&lt;/h2&gt;

&lt;p&gt;I deploy almost every agent as a Lambda triggered by EventBridge on a schedule, with state in Postgres (Supabase), secrets in AWS Secrets Manager, and observability through CloudWatch + a thin custom dashboard. Nothing exotic.&lt;/p&gt;

&lt;p&gt;A few opinions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale to zero or stay home.&lt;/strong&gt; If your agent runs 4 times a day, a 24/7 container is a waste. Lambda + EventBridge costs me cents per agent per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One agent per Lambda.&lt;/strong&gt; Co-locating agents to "save cold starts" creates dependency hell. Cold starts are 2 seconds. You will live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead-letter queue for everything.&lt;/strong&gt; Failed runs go to SQS with the full state. I review the DLQ weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags for tools.&lt;/strong&gt; I gate every new tool behind a flag. New tool ships disabled, gets enabled for one site, evaluated, then rolled out. This has caught two tool-design mistakes before they hit all sites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full production stack for a typical agent in my BizFlowAI ContentStudio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;EventBridge (cron)&lt;/span&gt;
  &lt;span class="s"&gt;-&amp;gt; Lambda (agent runner, max 15 min)&lt;/span&gt;
     &lt;span class="s"&gt;-&amp;gt; Postgres (state, memory, queues)&lt;/span&gt;
     &lt;span class="s"&gt;-&amp;gt; Claude API or local Ollama (LLM)&lt;/span&gt;
     &lt;span class="s"&gt;-&amp;gt; Tool Lambdas (publish, fetch, analyze)&lt;/span&gt;
  &lt;span class="s"&gt;-&amp;gt; CloudWatch (logs, metrics, alarms)&lt;/span&gt;
  &lt;span class="s"&gt;-&amp;gt; SQS DLQ (failed runs)&lt;/span&gt;
  &lt;span class="s"&gt;-&amp;gt; Dashboard (Next.js, reads Postgres)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I would do if I were starting today
&lt;/h2&gt;

&lt;p&gt;If you are building your first production agent in 2026, my opinionated shortlist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick one workflow that already exists as a human task.&lt;/strong&gt; Document it step by step. That document becomes your job spec and your eval set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the tools first, with tests.&lt;/strong&gt; Stub the agent loop. You can test 80% of the system without an LLM in the mix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Claude or GPT for the main loop, not a local model.&lt;/strong&gt; Local LLMs are great for embeddings and classification side-tasks; for the agent's reasoning you want the strongest model you can afford. The cost difference is smaller than the reliability difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard budgets in code, not in prompts.&lt;/strong&gt; Always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation before scale.&lt;/strong&gt; Do not deploy to 10 use cases until you have 50 eval cases passing on 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the handoff path on day one.&lt;/strong&gt; What happens when the agent gets stuck? Who or what picks it up? If the answer is "nothing", you will find out at 3am.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agents I trust to run while I sleep are not the smartest ones. They are the ones with the tightest tool contracts, the most boring control loop, and the eval set I actually run.&lt;/p&gt;

&lt;p&gt;If you are working on an agent that needs to leave the demo stage and survive in production, or you want a second pair of eyes on an architecture before you commit to it, I am happy to talk. You can reach me at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;, or browse more posts on the &lt;a href="https://lazar-milicevic.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt; where I write about RAG, evaluation, and the unglamorous parts of shipping AI systems.&lt;/p&gt;

</description>
      <category>howtocreateanaiagent</category>
      <category>aiagentinproduction</category>
      <category>llmtooldesign</category>
      <category>agentmemoryarchitecture</category>
    </item>
    <item>
      <title>The AI Implementation Process I Use With Every Client</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:24:31 +0000</pubDate>
      <link>https://dev.to/lamingsrb/the-ai-implementation-process-i-use-with-every-client-5a6i</link>
      <guid>https://dev.to/lamingsrb/the-ai-implementation-process-i-use-with-every-client-5a6i</guid>
      <description>&lt;h1&gt;
  
  
  The AI Implementation Process I Use With Every Client
&lt;/h1&gt;

&lt;p&gt;Most AI projects do not fail at the model. They fail in the six weeks before anyone writes a prompt, and in the six weeks after the demo lands in a Slack channel and nobody knows who owns it. I have run enough of these now (from one-off automations to multi-agent content systems running unattended) that the process has converged into something stable. This is the version I actually use.&lt;/p&gt;

&lt;p&gt;It has five phases: scoping, POC, integration, evaluation, operations. Each phase has an exit criterion. If we cannot meet the exit criterion, we do not move forward. That single rule has saved more projects than any clever architecture choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Scoping (1 to 2 weeks, fixed price)
&lt;/h2&gt;

&lt;p&gt;Scoping ends with a written document that names the workflow being automated, the system of record it touches, the success metric in hours or dollars, the data we have access to, and the smallest possible first slice. No model is chosen yet. No code is written. If we cannot produce that document, the engagement stops here and the client keeps the document.&lt;/p&gt;

&lt;p&gt;The hardest part of scoping is resisting the urge to solve the interesting problem. Clients almost always describe the AI-shaped fantasy ("an agent that handles all support tickets") when the real opportunity is narrower and uglier ("triage tier-1 tickets that mention billing, route to the right queue, draft a reply for human approval"). The narrower version ships. The fantasy does not.&lt;/p&gt;

&lt;p&gt;I run scoping as three sessions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Workflow walkthrough.&lt;/strong&gt; Someone who actually does the work shows me their screen for an hour. I record it. I take timestamps. The point is to find the moments where a human is doing pattern matching that an LLM can do, and to find the moments where they are doing judgment that an LLM should not do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data audit.&lt;/strong&gt; Where does the input live? Where does the output need to go? What is the auth story? If the data is locked inside a SaaS product with no API and no export, that is the project, and we deal with it now, not in week six.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROI sizing.&lt;/strong&gt; Hours per month times burdened hourly cost, minus realistic infrastructure and maintenance. If the answer is under $20k/year in savings, I usually tell the client to wait. The build and the babysitting are not worth it at that scale yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Exit criterion:&lt;/strong&gt; a one-page scope with a single first slice, a measurable success metric, and a named human owner on the client side. No owner, no project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Proof of Concept (2 to 4 weeks)
&lt;/h2&gt;

&lt;p&gt;The POC has one job: kill the project cheaply if it cannot work. I treat the POC as adversarial. I am trying to find the reason this will not ship, before we spend integration money on it.&lt;/p&gt;

&lt;p&gt;Concretely, a POC for me looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A thin script or notebook, not a product. No UI unless the UI is the risk.&lt;/li&gt;
&lt;li&gt;The real model we plan to use in production (Claude Sonnet, GPT-4 class, or a local Llama/Qwen via Ollama if data residency matters), not a cheaper proxy. Cheap models lie about feasibility.&lt;/li&gt;
&lt;li&gt;A hand-curated set of 30 to 50 real examples from the client's actual data, with expected outputs written by a human who knows the domain.&lt;/li&gt;
&lt;li&gt;A rough eval harness, even if it is just a spreadsheet with pass/fail and notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The POC answers four questions in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What "no" means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does the model produce the right shape of output reliably?&lt;/td&gt;
&lt;td&gt;Schema issues, structured-output failures. Fixable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does it produce the right &lt;em&gt;content&lt;/em&gt; on easy cases?&lt;/td&gt;
&lt;td&gt;Capability gap. Sometimes fixable with retrieval or examples.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does it handle the long tail without catastrophic failures?&lt;/td&gt;
&lt;td&gt;The real risk. Often the project killer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can we detect when it is wrong?&lt;/td&gt;
&lt;td&gt;If no, the project cannot ship to production. Full stop.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last question is the one most people skip. An AI system you cannot evaluate is an AI system you cannot trust, and an AI system you cannot trust is a demo, not a product. I have walked away from POCs that worked 90% of the time because there was no signal to catch the 10%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit criterion:&lt;/strong&gt; measurable performance on the eval set that the client agrees is good enough to justify integration cost, plus a documented failure mode list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Integration (3 to 8 weeks)
&lt;/h2&gt;

&lt;p&gt;This is where most of the actual work lives, and where most of my time goes. The model is usually the easy part by now. The integration is what makes it real.&lt;/p&gt;

&lt;p&gt;My default stack for production AI work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; simple, explicit code first. I reach for LangGraph or a hand-rolled state machine only when the workflow genuinely has branches and loops. Most "agents" are a sequential pipeline pretending to be agentic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Postgres for everything, with pgvector when retrieval matters. Supabase if the client wants managed. I do not introduce a separate vector DB until pgvector measurably stops scaling, which is later than people think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; hybrid search (BM25 + dense) with reciprocal rank fusion. Pure semantic search loses on exact identifiers, SKUs, error codes, names. Pure keyword loses on paraphrase. RRF is the cheap fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; AWS Lambda + EventBridge for scheduled and event-driven work, API Gateway when something needs to be called. Scales to zero, which matters for workloads that run hourly or in bursts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend (when needed):&lt;/strong&gt; Next.js with server actions. Boring is good here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three integration details I now treat as non-negotiable:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Idempotency keys on everything
&lt;/h3&gt;

&lt;p&gt;Any external action (send email, create ticket, post to CRM) gets an idempotency key derived from the input. Retries are inevitable, duplicate side effects are not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. A human-in-the-loop seam, even if unused
&lt;/h3&gt;

&lt;p&gt;I always build the approval queue before I build the auto-send. Even if the client wants full automation eventually, shipping with human review for the first 2 to 4 weeks catches the failure modes the eval set missed. Turning approval off later is one config change.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cost guardrails per workflow
&lt;/h3&gt;

&lt;p&gt;Token budgets per execution, hard cutoffs, alerts at 50/80/100% of monthly budget. I have seen a single retry loop burn $400 in an hour. Never again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit criterion:&lt;/strong&gt; the system runs end to end on real production data, with logging, retries, idempotency, and a kill switch. Not perfect outputs yet, but the pipes are sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: Evaluation (continuous, but formalized for 2 weeks)
&lt;/h2&gt;

&lt;p&gt;Evaluation is not a phase you finish. It is a system you build once and keep running forever. But there is a discrete block of work to set it up, and that is what this phase is.&lt;/p&gt;

&lt;p&gt;I build three layers of evaluation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Offline eval set.&lt;/strong&gt; The 30 to 50 examples from the POC, grown to 100 to 300, with expected outputs and a scoring rubric. Run on every prompt or model change. This is your regression test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-judge for open-ended outputs.&lt;/strong&gt; For anything where there is no single correct answer (drafted emails, summaries, classifications with reasoning), I use a separate, stronger model with a calibrated rubric to score outputs. I have written about how to actually calibrate this so the judge does not just rubber-stamp. The short version: you score the judge against human labels on a held-out set, and you do not trust a judge you have not calibrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production telemetry.&lt;/strong&gt; Every run logs inputs, outputs, model version, prompt version, latency, tokens, cost, and the downstream outcome (was the draft email sent as-is, edited, or rejected?). That last signal is gold. It is the closest thing to ground truth you get in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trap here is treating eval as a one-time gate. Models change. Prompts drift. Data shifts. The eval set has to be re-run on every change and the production telemetry has to feed back into growing the eval set. If a real production failure happens, it goes into the eval set the same day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit criterion:&lt;/strong&gt; the client can answer "is the system still working correctly?" without calling me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5: Operations and Handoff (2 to 4 weeks)
&lt;/h2&gt;

&lt;p&gt;This is the phase that separates a project that survives from one that dies six months in when something breaks and nobody knows where to look.&lt;/p&gt;

&lt;p&gt;What I deliver in operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runbook.&lt;/strong&gt; A markdown doc with the top 10 things that can go wrong, how to detect them, and how to fix them. Real ones, from this system, not generic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards.&lt;/strong&gt; Usually a simple internal page or a Grafana board: success rate, cost per day, queue depth, latency P50/P95, model errors. The client looks at this weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts.&lt;/strong&gt; Pager-worthy alerts on hard failures (pipeline stopped, cost spike, eval regression). Low-noise. If alerts cry wolf, they get muted, and then the real failure goes unnoticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned prompts and configs.&lt;/strong&gt; In git, with a changelog. Prompt changes are deploys, not Slack messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A maintenance retainer or a clean exit.&lt;/strong&gt; Either I stay on for a defined number of hours per month, or I hand off to an internal team with a transition period. No silent fade-outs. Those end badly for both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do differently if I were starting over
&lt;/h2&gt;

&lt;p&gt;A few opinions, after running this loop enough times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spend more on scoping, less on the POC.&lt;/strong&gt; A bad scope makes a great POC useless. I have never regretted an extra week of scoping. I have regretted skipping it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the boring model.&lt;/strong&gt; Use the strongest reliable model in your tier (Claude Sonnet or GPT-4 class) until you have a reason not to. Optimizing for cost too early picks fights you cannot win yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the eval before the agent.&lt;/strong&gt; Sounds backwards. It is not. If you cannot define what good looks like, you cannot build toward it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the first 30 days in production as part of the build.&lt;/strong&gt; Most of the real bugs surface there. Budget for it. Tell the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say no more often.&lt;/strong&gt; The projects I have turned down have, on average, been better decisions than the ones I took. Wrong-shaped projects do not get better with effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape of this process is not unique to my work. What is mine is the calibration: which phases I now know to invest in, which exit criteria I refuse to skip, and which mistakes I have made enough times to write them down. That last category is the actual deliverable when you hire someone like me, more than the code.&lt;/p&gt;

&lt;p&gt;If you are scoping an AI implementation and want a second pair of eyes on it before you commit budget, I am happy to look at it. Reach out at &lt;a href="https://lazar-milicevic.com/#contact" rel="noopener noreferrer"&gt;lazar-milicevic.com/#contact&lt;/a&gt;, or browse the rest of the blog for more on evaluation, RAG, and getting agents into production.&lt;/p&gt;

</description>
      <category>aiimplementationprocess</category>
      <category>aipocdevelopment</category>
      <category>llminproduction</category>
      <category>ragpipeline</category>
    </item>
    <item>
      <title>What Is Process Automation? A Practical Guide</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:11:44 +0000</pubDate>
      <link>https://dev.to/lamingsrb/what-is-process-automation-a-practical-guide-41dp</link>
      <guid>https://dev.to/lamingsrb/what-is-process-automation-a-practical-guide-41dp</guid>
      <description>&lt;h1&gt;
  
  
  What Is Process Automation? A Practical Guide
&lt;/h1&gt;

&lt;p&gt;You're a founder paying yourself last while still spending three hours every Friday reconciling Stripe payouts against invoices in QuickBooks. You know it should be automated. You've heard "BPA," "RPA," and "workflow automation" used interchangeably by vendors who all want a $500/month seat. This guide cuts through that.&lt;/p&gt;

&lt;p&gt;Process automation, in plain terms, is using software to do the repetitive work a human used to do — receiving a form, updating a record, sending a notification, moving a file, generating a report. The hard part isn't the tools. The hard part is choosing the right kind of automation for the work in front of you, and not over-engineering it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What process automation actually means
&lt;/h2&gt;

&lt;p&gt;Process automation is the use of software to execute a defined sequence of steps end-to-end, with little or no human input, against a measurable business outcome (an invoice paid, a lead routed, a ticket resolved). It is not "AI doing your job." It is a deterministic pipeline that triggers on an event, performs steps, and ends in a known state — sometimes with an AI step inside it.&lt;/p&gt;

&lt;p&gt;Three things have to be true for something to be a candidate for automation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The process is repeatable.&lt;/strong&gt; You can describe the steps without saying "it depends" four times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The inputs are structured or can be made structured.&lt;/strong&gt; A PDF, a webhook, a form submission, a database row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The outcome is verifiable.&lt;/strong&gt; You can tell, programmatically, whether it worked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a process fails one of those three, automate the parts that pass and leave the rest to a human. That's where most automation projects die — trying to automate decisions that should still be human-reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  BPA vs RPA vs workflow automation: what's the difference
&lt;/h2&gt;

&lt;p&gt;These three terms get sold as if they mean the same thing. They don't. Picking the wrong category will cost you money and time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it automates&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Tools (examples)&lt;/th&gt;
&lt;th&gt;Typical limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workflow automation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Steps inside or between SaaS apps via APIs&lt;/td&gt;
&lt;td&gt;"When X happens in app A, do Y in app B"&lt;/td&gt;
&lt;td&gt;Zapier, Make, n8n, Pipedream&lt;/td&gt;
&lt;td&gt;Breaks when an API doesn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RPA (Robotic Process Automation)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI-level actions on apps that have no API&lt;/td&gt;
&lt;td&gt;Legacy systems, desktop apps, scraping a portal&lt;/td&gt;
&lt;td&gt;UiPath, Automation Anywhere, Power Automate Desktop&lt;/td&gt;
&lt;td&gt;Brittle when UIs change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BPA (Business Process Automation)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;End-to-end processes spanning multiple teams/systems&lt;/td&gt;
&lt;td&gt;Onboarding, procurement, AR/AP, compliance flows&lt;/td&gt;
&lt;td&gt;Camunda, ServiceNow, Pega&lt;/td&gt;
&lt;td&gt;Heavier setup, needs process modeling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI/agentic automation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Steps that require interpretation (unstructured input, decisions)&lt;/td&gt;
&lt;td&gt;Email triage, document extraction, classification&lt;/td&gt;
&lt;td&gt;LLM APIs orchestrated by code or platforms&lt;/td&gt;
&lt;td&gt;Non-determinism, needs guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A real automation usually mixes categories. Example: a webhook fires from Stripe (workflow), an LLM extracts line items from a vendor PDF (AI), the result is written to QuickBooks via API (workflow), and once a quarter an RPA bot exports a report from a legacy bank portal that still doesn't have an API in 2026.&lt;/p&gt;

&lt;p&gt;If your process lives entirely inside modern SaaS, you almost never need RPA. If you're stuck reading from a desktop app or a portal with no integration, RPA is sometimes the only door. If you're orchestrating something across finance, ops, and sales with approvals at each stage, that's BPA territory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start: pick a process worth automating
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake I see solo founders and ops people make is automating something interesting instead of something expensive. Use this scoring approach on a sticky note before you write a line of code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequency:&lt;/strong&gt; How often does this run? (per day / week / month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minutes per run:&lt;/strong&gt; How long does the human take?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error cost:&lt;/strong&gt; What happens if a step is missed? ($, churn, compliance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability:&lt;/strong&gt; How often does the process change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply frequency × minutes × 52 to get yearly hours. If that's under 20 hours/year, leave it alone unless the error cost is high. If it's over 100 hours/year and stable, automate it this month.&lt;/p&gt;

&lt;p&gt;A simple Python sketch for prioritizing a backlog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;processes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_routing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly_report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_offboarding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;yearly_hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yearly_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; hours/yr=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;yearly_hours&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort by score. Automate top of the list. Don't get fancy yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four examples that are worth building
&lt;/h2&gt;

&lt;p&gt;These are the patterns I see ship for solopreneurs and small teams. They're not exciting, which is exactly why they pay back.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inbox triage and reply drafting
&lt;/h3&gt;

&lt;p&gt;Trigger: a new email lands. Steps: classify (sales lead, support, vendor, noise), extract key fields (company, ask, urgency), draft a reply in your voice, attach it as a Gmail draft. Human still hits send.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.new_message&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;classify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lead&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;support&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;vendor&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;billing&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;noise&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category == "lead"&lt;/span&gt;
    &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;company&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ask&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;budget_signal&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;draft_reply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direct,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fluff,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mention&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pricing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;page"&lt;/span&gt;
      &lt;span class="na"&gt;save_as&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.draft&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category == "support"&lt;/span&gt;
    &lt;span class="na"&gt;create_ticket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linear&lt;/span&gt;
    &lt;span class="na"&gt;draft_reply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support_ack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "human sends" rule is doing a lot of work here. It catches LLM mistakes and keeps your reputation intact while you tune the prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Lead-to-CRM with enrichment
&lt;/h3&gt;

&lt;p&gt;Form submission → enrich company from domain → score → route to the right rep → notify in Slack with a one-line summary. This is the highest-ROI automation for any business with inbound leads, because every minute a lead waits costs conversion. The &lt;a href="https://hbr.org/2011/03/the-short-life-of-online-sales-leads" rel="noopener noreferrer"&gt;classic MIT/InsideSales study&lt;/a&gt; found odds of qualifying a lead drop sharply after the first hour — even if the exact multiplier varies by industry, the direction is settled.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AR/AP: invoices in, invoices out
&lt;/h3&gt;

&lt;p&gt;Vendor invoice arrives by email → PDF parsed → line items extracted → matched against PO → written to accounting system → flagged for approval if over a threshold. On the AR side: recurring invoices generated from contract data, sent on schedule, with dunning emails staged for overdue accounts.&lt;/p&gt;

&lt;p&gt;This is the kind of work that quietly eats 4–8 hours a week in a 5-person company. It's also boring enough that nobody volunteers to own it, which is why it stays manual for years.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Reporting that builds itself
&lt;/h3&gt;

&lt;p&gt;Pull metrics from Stripe, your product DB, and your ad platforms every Monday at 7am. Compute deltas. Generate a markdown summary. Post it in Slack and email it to yourself. No more "I should really look at the dashboard."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# weekly_report.py — runs via cron Monday 07:00
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;

&lt;span class="n"&gt;week&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;mrr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mrr_snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;churn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;churn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dau_avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write a 5-bullet summary for a solo founder.
Numbers: MRR=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mrr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, churn=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;churn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, signups=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signups&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, DAU avg=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.
Compare to prior week. Flag anomalies. No fluff.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The architecture that doesn't fall over
&lt;/h2&gt;

&lt;p&gt;After you've built five or six of these, you'll notice the same architecture under all of them. Build with it from day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐   ┌──────────────┐   ┌─────────────┐   ┌────────┐
│ Trigger  │──▶│ Orchestrator │──▶│ Steps       │──▶│ Sink   │
│ webhook/ │   │ (queue +     │   │ (API calls, │   │ (DB,   │
│ cron/    │   │  retries +   │   │  LLM, RPA   │   │ Slack, │
│ email    │   │  logs)       │   │  bot)       │   │ CRM)   │
└──────────┘   └──────────────┘   └─────────────┘   └────────┘
                      │
                      ▼
                 ┌─────────┐
                 │ Audit   │  every run, every step, every input/output
                 │ log     │
                 └─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-negotiable pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency keys.&lt;/strong&gt; If a webhook fires twice, you should not invoice the customer twice. Use the event ID as a dedup key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries with backoff.&lt;/strong&gt; APIs fail. Plan for it. 3 retries, exponential backoff, dead-letter queue for the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs.&lt;/strong&gt; Every step writes input, output, duration, and status. When something goes wrong at 2am on a Saturday, you need to be able to answer "what happened?" without re-running anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A kill switch.&lt;/strong&gt; One env var, one config flag, one button. When the LLM hallucinates a refund, you want to stop the pipeline in under a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop on anything irreversible.&lt;/strong&gt; Sending money, deleting data, emailing a customer — require approval until you've watched it run clean for weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As Werner Vogels put it bluntly: &lt;a href="https://www.allthingsdistributed.com/2008/12/eventually_consistent.html" rel="noopener noreferrer"&gt;"everything fails, all the time."&lt;/a&gt; Build like you believe him.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that kill automation projects
&lt;/h2&gt;

&lt;p&gt;I've watched these patterns burn through budgets at companies from 2-person shops to 200-person ops teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automating before mapping.&lt;/strong&gt; If you can't write the process on a whiteboard in 10 minutes, you can't automate it. Write it first. Most "automation problems" are actually undocumented-process problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimizing rare work.&lt;/strong&gt; That 90-minute task you do once a quarter is not worth a sprint. The 4-minute task you do 30 times a day is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One giant flow.&lt;/strong&gt; A 47-step Zap is a debugging nightmare. Break processes into small pipelines that pass state to each other through a queue or a database. Each pipeline does one thing and can be tested in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No observability.&lt;/strong&gt; If your only signal that automation failed is a customer complaining, you've built a liability. Log every run. Alert on failures. Track success rate weekly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM where regex would do.&lt;/strong&gt; If you're parsing a fixed-format invoice number, don't burn tokens on it. Use the LLM for the messy parts (unstructured email bodies, varying PDF layouts) and deterministic code for everything else. This also keeps your costs predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Believing the demo.&lt;/strong&gt; Every no-code tool looks great in a 90-second video. Build a real flow with your real data before you commit to a $200/month plan. Most pilots fail not because the tool can't do it, but because the team's processes weren't ready — a pattern Gartner and others have flagged in &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2021-09-21-gartner-says-worldwide-robotic-process-automation-software-revenue-to-reach-nearly-2-billion-in-2021" rel="noopener noreferrer"&gt;RPA program research&lt;/a&gt; for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30-day plan to ship your first three automations
&lt;/h2&gt;

&lt;p&gt;You don't need a transformation initiative. You need three weeks and a coffee budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1 — Map and pick.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List every repetitive task you or your team did this week.&lt;/li&gt;
&lt;li&gt;Score them with the formula above.&lt;/li&gt;
&lt;li&gt;Pick the top three. Write each process down step by step. Note the inputs, outputs, and edge cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 2 — Build the smallest version.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build automation #1 end-to-end, even if half of it is just "post a message in Slack with the data for a human to act on."&lt;/li&gt;
&lt;li&gt;Run it manually-triggered for a few days. Watch where it breaks.&lt;/li&gt;
&lt;li&gt;Add retries, logging, and a kill switch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 3 — Harden and expand.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move automation #1 to its real trigger (webhook, cron, email).&lt;/li&gt;
&lt;li&gt;Build automation #2 using the same scaffolding (queue, logs, retries).&lt;/li&gt;
&lt;li&gt;Add a weekly review: success rate, failures, time saved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 4 — Measure and decide what's next.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculate actual hours saved per week.&lt;/li&gt;
&lt;li&gt;Pick automation #4 from your backlog, or harden what you have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If after 30 days you haven't saved at least 5 hours/week, you picked the wrong processes. Go back to the scoring step.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BizFlowAI approaches this
&lt;/h2&gt;

&lt;p&gt;We build process automations for solopreneurs and small teams — not platforms, not seats, not a course. The pattern is consistent across every engagement: map the process, score it honestly, build the smallest version that ships, add the boring infrastructure (idempotency, retries, audit logs, kill switches), then layer AI only where deterministic code can't reach. Most of our wins come from inbox triage, lead routing, AR/AP, and reporting pipelines — the unglamorous work that adds up to a day a week back.&lt;/p&gt;

&lt;p&gt;We default to standard tools (n8n, Python workers, Postgres, the major LLM APIs) so clients aren't locked into a proprietary runtime they can't maintain. If you want to see what this looks like applied to your actual workflows, that's the conversation we're best at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Work with BizFlowAI
&lt;/h2&gt;

&lt;p&gt;If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://calendly.com/lamingsrb" rel="noopener noreferrer"&gt;Book a free discovery call&lt;/a&gt;&lt;/strong&gt; — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.&lt;/p&gt;

&lt;p&gt;More guides like this on the &lt;a href="https://dev.to/blog"&gt;BizFlowAI blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>whatisprocessautomation</category>
      <category>bpavsrpavsworkflowautomation</category>
      <category>businessprocessautomationguide</category>
      <category>workflowautomationtools</category>
    </item>
    <item>
      <title>Make.com vs n8n: I Built the Same AI Agent in Both</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:11:40 +0000</pubDate>
      <link>https://dev.to/lamingsrb/makecom-vs-n8n-i-built-the-same-ai-agent-in-both-2hc8</link>
      <guid>https://dev.to/lamingsrb/makecom-vs-n8n-i-built-the-same-ai-agent-in-both-2hc8</guid>
      <description>&lt;h1&gt;
  
  
  Make.com vs n8n: I Built the Same AI Agent in Both
&lt;/h1&gt;

&lt;p&gt;A client needed a lead-routing agent. Gmail in, AI classifies by industry and urgency, hot leads to a sales rep on Telegram, cold ones to the CRM with a tagged follow-up. Nine steps. I built it three times — Make, n8n, then raw code — because I picked wrong the first two times. Here is the decision rule I wish someone had given me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exact workflow I rebuilt three times
&lt;/h2&gt;

&lt;p&gt;Before I argue about tooling, here is the spec, because vague comparisons are useless. Nine nodes total: Gmail trigger → extract sender + body → OpenAI classification (industry, urgency 1-5) → branch on urgency → Telegram message to sales rep (hot) → CRM upsert with tag (cold) → follow-up sequence enqueue → log to Postgres → error catch.&lt;/p&gt;

&lt;p&gt;That is the surface. What killed me was the second-order work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry logic&lt;/strong&gt; when OpenAI returns a 429 or the CRM webhook 502s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom HTTP call&lt;/strong&gt; to an invoicing API with a non-standard signed auth header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume&lt;/strong&gt;: roughly 40,000 executions/month once you count retries and polling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every comparison post online benchmarks the happy path. Real client work is the unhappy path. That is where Make and n8n diverge hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Make actually beats n8n (I will say it out loud)
&lt;/h2&gt;

&lt;p&gt;For a workflow with under ten steps, no retry logic, no custom auth, and under 1,000 ops/month, Make is the better choice. The UX is genuinely cleaner and you will ship faster. I built the happy-path version of this agent in Make in eight minutes flat.&lt;/p&gt;

&lt;p&gt;Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Gmail trigger authenticated in two clicks. n8n's OAuth flow took me about four minutes the first time because you have to register a Google Cloud project.&lt;/li&gt;
&lt;li&gt;The OpenAI module has a model dropdown and a structured-output toggle. No JSON wrangling.&lt;/li&gt;
&lt;li&gt;The left-to-right scenario canvas reads like a sentence. When I screen-share with non-technical clients, they understand a Make scenario without me explaining.&lt;/li&gt;
&lt;li&gt;Built-in modules for Telegram, HubSpot, Pipedrive, Notion — all pre-authed with sensible defaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your automation looks like &lt;em&gt;"trigger → enrich → send"&lt;/em&gt; and runs a few hundred times a month, stop reading and use Make. Your time is worth more than the $9–29/month subscription. Most YouTubers who tell you "always use n8n" are selling a self-hosting course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Make falls off a cliff: retries, custom auth, and ops pricing
&lt;/h2&gt;

&lt;p&gt;Make's pricing model is per-operation, and every error handler, every router branch, every iteration consumes ops. The moment you add real production logic — retries, dead-letter queues, idempotency checks — the ops counter accelerates faster than your feature set.&lt;/p&gt;

&lt;p&gt;Here is what happened on this specific client:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client asked for retry logic. If the OpenAI call fails or the CRM is down, the lead can't vanish.&lt;/li&gt;
&lt;li&gt;Make's native retry is limited to a few attempts on a small subset of modules. For OpenAI failures, I had to build error handlers that route to a data store.&lt;/li&gt;
&lt;li&gt;Each error handler branch costs operations against the monthly quota.&lt;/li&gt;
&lt;li&gt;To replay failed runs, I built a separate scenario that polls the data store every five minutes. That scenario alone burned ~8,600 ops/month doing nothing useful.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At around 1,200 real-traffic ops/month plus the polling overhead, the workflow needed the Pro tier. Make's UX is great until you're paying for the privilege of working around its limitations.&lt;/p&gt;

&lt;p&gt;Then came the invoicing API. Non-standard auth header, HMAC-signed payload. In Make, the HTTP module on the lower tiers cannot generate the signature inline — you'd need a separate scenario, a webhook, and a data store hop. Ugly. In n8n:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// n8n Function node — 6 lines&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Timestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. That single capability — drop into JavaScript or Python when the visual nodes can't express what you need — is the deciding factor for any non-trivial workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost math nobody puts in a comparison post
&lt;/h2&gt;

&lt;p&gt;This is where solopreneurs actually feel the choice. Real numbers from this client, audited from billing dashboards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Make.com (Pro)&lt;/th&gt;
&lt;th&gt;n8n self-hosted&lt;/th&gt;
&lt;th&gt;n8n Cloud (Pro)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Executions/month (with retries + polling)&lt;/td&gt;
&lt;td&gt;~40,000 ops&lt;/td&gt;
&lt;td&gt;~40,000&lt;/td&gt;
&lt;td&gt;~40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$47&lt;/td&gt;
&lt;td&gt;$0 marginal*&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual cost&lt;/td&gt;
&lt;td&gt;$564&lt;/td&gt;
&lt;td&gt;~$0&lt;/td&gt;
&lt;td&gt;$600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom code support&lt;/td&gt;
&lt;td&gt;Paid tier, limited&lt;/td&gt;
&lt;td&gt;Native (JS + Python)&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native retry/error workflows&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host option&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Marginal cost. My home server (mini PC, 32GB RAM, Ubuntu under WSL on one box, bare-metal Debian on another) runs eight other things. Amortized hardware is roughly $4/month of electricity.&lt;/p&gt;

&lt;p&gt;Over a year, that's $564 saved on &lt;strong&gt;one&lt;/strong&gt; client workflow. Run five client workflows and you've paid for a developer-grade server. Run ten and you've funded a year of API credits.&lt;/p&gt;

&lt;p&gt;The Make pricing trap is not the headline number. It's that every architectural improvement you make — better error handling, idempotency, observability — costs you ops. n8n charges you once for the engine, then your improvements are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting n8n in 12 minutes (the actual setup)
&lt;/h2&gt;

&lt;p&gt;If you're going the n8n route, here is the Docker Compose I use on the client server. Postgres for persistence, n8n behind a Caddy reverse proxy for TLS, encrypted credentials at rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./pgdata:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresdb&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;automation.yourdomain.com&lt;/span&gt;
      &lt;span class="na"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://automation.yourdomain.com/&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;EXECUTIONS_DATA_PRUNE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;EXECUTIONS_DATA_MAX_AGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;  &lt;span class="c1"&gt;# 7 days&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./n8n-data:/home/node/.n8n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;

docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things I learned the hard way running this in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always set &lt;code&gt;EXECUTIONS_DATA_PRUNE&lt;/code&gt;.&lt;/strong&gt; Without it, the Postgres db grew 400MB/week on this client's workload. After a year the n8n UI crawled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the queue execution mode&lt;/strong&gt; (&lt;code&gt;EXECUTIONS_MODE=queue&lt;/code&gt; with Redis) once you're past 10k executions/day. Single-process mode will block on a slow webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up &lt;code&gt;n8n-data&lt;/code&gt; and the Postgres dump nightly.&lt;/strong&gt; Credentials are AES-encrypted with &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; — lose that key, lose every saved credential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put it behind Cloudflare Tunnel or Tailscale&lt;/strong&gt;, not a raw exposed port. n8n's UI authentication is fine but webhooks are public by design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twelve minutes from &lt;code&gt;docker compose up&lt;/code&gt; to a working webhook in production, if your DNS and TLS are already set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule I give every client
&lt;/h2&gt;

&lt;p&gt;Skip the holy war. Here is the actual rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer than 10 steps, no retries, no custom auth, under 1,000 ops/month&lt;/strong&gt; → Make. Ship in an hour. Worth the $9–29.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching, retries, custom APIs, or any real volume&lt;/strong&gt; → n8n from day one. Self-hosted if you have a server, cloud if you don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow needs version control, code review, CI/CD, or multi-environment deploys&lt;/strong&gt; → skip both. Write it in Python with a task queue. The visual tools become friction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trap is building in Make first and migrating later. I have done that rebuild three times. It is always two full days I don't get back, because Make's data shapes don't map cleanly to n8n's, and your error handlers are scenario-specific.&lt;/p&gt;

&lt;p&gt;The Make-vs-n8n debate is the wrong question. The right question: &lt;em&gt;does my workflow need logic Make can't express cleanly?&lt;/em&gt; If yes, n8n today. If no, Make today. Pick once, build once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick gut check before you start building
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Will I ever call an API with a custom signed header? → n8n&lt;/li&gt;
&lt;li&gt;Do I need to retry failed runs without burning ops? → n8n&lt;/li&gt;
&lt;li&gt;Will I run more than 5,000 executions a month? → n8n&lt;/li&gt;
&lt;li&gt;Is the workflow under 10 steps and will stay there? → Make&lt;/li&gt;
&lt;li&gt;Will a non-technical client need to read the canvas? → Make&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why bizflowai.io helps with this
&lt;/h2&gt;

&lt;p&gt;For client workflows we run through bizflowai.io, we default to n8n self-hosted on a managed server because the retry/custom-auth/volume math almost always wins for lead-routing, invoicing, and CRM-sync automations. We still build in Make when the workflow is genuinely simple and the client wants to own the canvas themselves. The honest part of the job is telling the client which tool fits before the build, not after — that one conversation saves the two-day rebuild every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want more like this?
&lt;/h2&gt;

&lt;p&gt;I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube.com/@bizflowai.io" rel="noopener noreferrer"&gt;Subscribe to bizflowai.io on YouTube&lt;/a&gt;&lt;/strong&gt; — never miss a new tutorial.&lt;/p&gt;

&lt;p&gt;Planning an AI automation project or need a second opinion on your architecture?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/lazar-m-919853111" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;&lt;/strong&gt; — Lazar Milicevic, GenAI Engineer &amp;amp; bizflowai.io Founder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Visit bizflowai.io&lt;/a&gt; for our services, case studies, and AI consulting.&lt;/p&gt;

</description>
      <category>n8nvsmake</category>
      <category>workflowautomation</category>
      <category>selfhostedn8n</category>
      <category>leadroutingautomation</category>
    </item>
    <item>
      <title>How Shopify Built a Model-Agnostic AI Stack</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:46:11 +0000</pubDate>
      <link>https://dev.to/lamingsrb/how-shopify-built-a-model-agnostic-ai-stack-2kfg</link>
      <guid>https://dev.to/lamingsrb/how-shopify-built-a-model-agnostic-ai-stack-2kfg</guid>
      <description>&lt;h1&gt;
  
  
  How Shopify Built a Model-Agnostic AI Stack
&lt;/h1&gt;

&lt;p&gt;You're three months into building an AI feature. It works. Customers love it. Then your model provider emails: "We're deprecating this version in 60 days." Or worse — the API just starts returning 500s on a Tuesday morning and your support inbox explodes.&lt;/p&gt;

&lt;p&gt;This is the reality of building on LLMs in 2026. Shopify hit this wall early, and instead of betting on one provider, they built an internal proxy that treats models as interchangeable parts. Here's how it works, why it matters, and how to build the same pattern into your stack — even if you're a team of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: every LLM is a moving target
&lt;/h2&gt;

&lt;p&gt;Models get deprecated. Providers change pricing without warning. Rate limits shift. A vendor you trusted last quarter quietly degrades quality on the model you fine-tuned your prompts around. If your production code has &lt;code&gt;client = anthropic.Anthropic()&lt;/code&gt; hard-coded at the top of every file, you have a fragility problem disguised as a dependency.&lt;/p&gt;

&lt;p&gt;Shopify's engineering team has talked openly about this. In public conference talks and engineering blog posts, their AI infrastructure team has described a pattern where every internal AI call routes through a single proxy layer. When Anthropic ships a new Claude version, when OpenAI deprecates an older endpoint, when a regional outage hits one provider, the proxy reroutes traffic. Application code never changes.&lt;/p&gt;

&lt;p&gt;This is not exotic infrastructure. It's the same pattern that made cloud-native apps survive AWS region outages: abstract the dependency, route around failure. The novelty is applying it to LLMs, where the "service" you're calling has unstable APIs, unstable pricing, and a habit of disappearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a model-agnostic proxy actually does
&lt;/h2&gt;

&lt;p&gt;An LLM proxy is a thin service that sits between your application code and any model provider (Anthropic, OpenAI, Google, Mistral, a local Ollama instance, whatever). Your app calls one endpoint with a generic request format. The proxy decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which provider gets the request right now&lt;/li&gt;
&lt;li&gt;What to do if that provider returns an error or times out&lt;/li&gt;
&lt;li&gt;Whether to retry on the same model or fall back to a different one&lt;/li&gt;
&lt;li&gt;How to normalize the response so your app gets a consistent shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The minimum viable version is maybe 200 lines of Python. The mature version — what Shopify and companies like Anthropic-customer Notion run — adds caching, observability, cost tracking per team, prompt versioning, and policy enforcement.&lt;/p&gt;

&lt;p&gt;Here's the core abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What your app code looks like with a proxy
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_email_classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: no model name, no provider SDK, no API key handling. The proxy maps &lt;code&gt;task&lt;/code&gt; to a routing policy. If &lt;code&gt;customer_email_classification&lt;/code&gt; is currently configured to use Claude Sonnet with GPT-4o as fallback, that's a config change, not a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the routing layer: a minimal proxy in Python
&lt;/h2&gt;

&lt;p&gt;Here's a stripped-down version of the pattern. This isn't production-ready — it's the skeleton to understand the shape.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LLMProxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;]]):&lt;/span&gt;
        &lt;span class="c1"&gt;# routing_table maps task names to ordered fallback chains
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;routing_table&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No route for task &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All models failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Ship to your logging/metrics pipeline
&lt;/span&gt;        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the routing config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;routing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_email_classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_openai&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama-3.1-70b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_ollama&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long_document_summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4-turbo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_openai&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLMProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Claude Sonnet 4 gets deprecated, you change one line in the routing table. When a provider starts returning 529 overloaded errors at 3pm every day, the second model in the chain catches the requests automatically. Your application code doesn't know and doesn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "failover" really has to handle
&lt;/h2&gt;

&lt;p&gt;Naive failover — "if request fails, try the next model" — is not enough. Real production failover handles at least these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Naive retry&lt;/th&gt;
&lt;th&gt;Proper handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5xx from provider&lt;/td&gt;
&lt;td&gt;Retry once on same model&lt;/td&gt;
&lt;td&gt;Fast-fail after 1 retry, switch provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit (429)&lt;/td&gt;
&lt;td&gt;Retry with backoff&lt;/td&gt;
&lt;td&gt;Switch provider immediately, don't wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;Retry&lt;/td&gt;
&lt;td&gt;Switch provider, log the slow one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad JSON / schema violation&lt;/td&gt;
&lt;td&gt;Crash&lt;/td&gt;
&lt;td&gt;Reroute to a model better at structured output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality degradation&lt;/td&gt;
&lt;td&gt;Invisible&lt;/td&gt;
&lt;td&gt;Periodic eval suite catches regressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model deprecated&lt;/td&gt;
&lt;td&gt;Production breaks&lt;/td&gt;
&lt;td&gt;Routing table flagged before sunset date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality dimension is the hard one. A provider can return a 200 OK with technically valid output that is subtly worse than what you got last week. Shopify and similar shops handle this with offline eval suites: a fixed set of representative prompts is rerun against every model in the chain on a schedule, and outputs are scored (either by another model or by humans on edge cases). When a model's score drops below threshold, it gets demoted in the routing table before users notice.&lt;/p&gt;

&lt;p&gt;You don't need this on day one. You do need a way to swap models in 5 minutes when something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalizing requests and responses across providers
&lt;/h2&gt;

&lt;p&gt;Anthropic's API and OpenAI's API are similar but not identical. System prompts go in different places. Tool-calling schemas differ. Streaming chunk formats differ. Token usage is reported differently. If your app code handles any of this directly, the proxy isn't actually decoupling anything.&lt;/p&gt;

&lt;p&gt;The fix is a normalization layer inside the proxy. Define one internal request shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extract_invoice_fields"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You extract structured data from invoices."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save_invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one internal response shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the proxy, you have adapters per provider that translate to/from these shapes. This is unglamorous code, but it's the work that buys you the freedom to swap providers without touching application logic. The LiteLLM and OpenRouter open-source projects already do most of this if you don't want to write adapters yourself — both are worth evaluating before you build your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and observability: the underrated wins
&lt;/h2&gt;

&lt;p&gt;Once everything routes through one place, two things become trivial that were previously impossible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-team, per-feature cost tracking.&lt;/strong&gt; Every call carries a &lt;code&gt;task&lt;/code&gt; label. Aggregate token usage by task and you know exactly which feature is costing what. Most teams discover that 80% of their LLM spend comes from 2-3 features they didn't expect. According to Anthropic's published pricing pages and OpenAI's pricing pages (always check the current ones — they change), input vs. output token costs can differ by 5x, and switching one high-volume feature from a premium model to a smaller one can cut a monthly bill meaningfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single audit log.&lt;/strong&gt; Every prompt, every response, every model used. When a customer reports a bad output from your AI feature three weeks ago, you can find the exact request and response. When legal asks "do we ever send customer PII to OpenAI," you can answer with data instead of a shrug.&lt;/p&gt;

&lt;p&gt;This second one matters more for compliance-sensitive industries (healthcare, finance, anything touching personal data under frameworks like HIPAA or GDPR). A proxy gives you one chokepoint for redaction, logging, and policy enforcement instead of N integration points across your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: the quietly massive cost lever
&lt;/h2&gt;

&lt;p&gt;Identical prompts get identical-ish responses. A proxy is the natural place to cache. Two layers worth implementing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exact-match cache.&lt;/strong&gt; Hash the full request (system + messages + temperature). If you've seen it in the last N hours, return the cached response. Useful for deterministic prompts at temperature 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic cache.&lt;/strong&gt; Embed the user prompt, look up similar prior prompts, and reuse the response if similarity is above a threshold. Useful for FAQ-style use cases where users ask the same thing five ways.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anthropic also offers prompt caching at the API level — long system prompts and tool definitions can be cached server-side for a fraction of the input token cost. Your proxy should pass the right cache-control headers to take advantage of this. Check Anthropic's current prompt caching documentation for the exact discount and TTL.&lt;/p&gt;

&lt;p&gt;Semantic caching is risky for anything where wrong answers matter (medical, legal, financial). For low-stakes use cases — customer support FAQ, internal docs Q&amp;amp;A — it can cut LLM spend significantly with minimal quality loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical adoption path for small teams
&lt;/h2&gt;

&lt;p&gt;You are not Shopify. You don't need a dedicated AI platform team. Here's how to get most of the benefit in a week of work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick a wrapper.&lt;/strong&gt; LiteLLM gives you a drop-in OpenAI-compatible interface that proxies to ~100 providers with retry and fallback built in. OpenRouter does similar with a hosted model. For most solo operators and small teams, one of these gets you 80% of the value with zero infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Centralize the calls.&lt;/strong&gt; Wrap every LLM call in your app behind one internal function. No &lt;code&gt;openai.chat.completions.create(...)&lt;/code&gt; scattered across 14 files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define your tasks.&lt;/strong&gt; Name each LLM use case (&lt;code&gt;classify_lead&lt;/code&gt;, &lt;code&gt;summarize_ticket&lt;/code&gt;, &lt;code&gt;draft_reply&lt;/code&gt;). Map each to a primary model and one fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log everything.&lt;/strong&gt; Task name, provider, model, latency, token counts, success/failure. A simple Postgres table works. You'll thank yourself in three months.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up a deprecation alert.&lt;/strong&gt; Subscribe to your providers' changelog feeds or RSS. When a model you use shows up on a deprecation list, you have weeks to swap it in the routing table — not hours after production breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eval suite, eventually.&lt;/strong&gt; Pick 20-50 representative prompts per task. Run them against each model in your chain on a monthly cadence. Score outputs (model-judged is fine to start). When scores drift, investigate.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1-5 are achievable in a week for a solo developer. Step 6 is what separates teams that get bitten by silent quality regressions from teams that don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BizFlowAI approaches this
&lt;/h2&gt;

&lt;p&gt;This proxy-and-failover pattern is the default for every client agent we build. When we ship a customer-support automation or a lead-qualification agent, the LLM calls go through a routing layer with primary + fallback providers, structured logging, and a config-driven model map. When Anthropic or OpenAI deprecates a model — and they will — the swap is a config change reviewed in minutes, not an incident.&lt;/p&gt;

&lt;p&gt;If you're running AI features in production and the thought of your primary provider going down for an afternoon makes you nervous, that's a fixable problem. &lt;a href="https://bizflowai.io" rel="noopener noreferrer"&gt;Book a call&lt;/a&gt; and we'll map out where your stack is fragile and what the smallest version of this proxy pattern looks like for your specific workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Treating LLMs as commodity inputs — not load-bearing dependencies — is the cheapest insurance you can buy against a market where models get deprecated, repriced, and rate-limited on someone else's schedule. The proxy pattern Shopify uses is not exotic. It's the same abstraction that made cloud apps survive their cloud providers' worst days, applied to a new dependency that happens to be more unstable than EC2 ever was.&lt;/p&gt;

&lt;p&gt;You don't need a platform team to do this. You need one wrapper, one routing table, and a logging table. Build it once. Swap models forever.&lt;/p&gt;

&lt;p&gt;Related reading on this blog: &lt;a href="https://bizflowai.io/blog/bizflowai-vs-zapier-2026" rel="noopener noreferrer"&gt;BizFlowAI vs Zapier (2026): AI-Native Automation&lt;/a&gt; and &lt;a href="https://bizflowai.io/blog/3-agent-loop-solo-agency" rel="noopener noreferrer"&gt;The 3-Agent Loop That Runs My Solo Agency&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Work with BizFlowAI
&lt;/h2&gt;

&lt;p&gt;If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://calendly.com/lamingsrb" rel="noopener noreferrer"&gt;Book a free discovery call&lt;/a&gt;&lt;/strong&gt; — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.&lt;/p&gt;

&lt;p&gt;More guides like this on the &lt;a href="https://dev.to/blog"&gt;BizFlowAI blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llmproxy</category>
      <category>modelagnosticai</category>
      <category>llmfailover</category>
      <category>llmgateway</category>
    </item>
    <item>
      <title>Token Sprawl Is Real. Here's How to Cap It.</title>
      <dc:creator>lamingsrb</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:46:03 +0000</pubDate>
      <link>https://dev.to/lamingsrb/token-sprawl-is-real-heres-how-to-cap-it-1hhd</link>
      <guid>https://dev.to/lamingsrb/token-sprawl-is-real-heres-how-to-cap-it-1hhd</guid>
      <description>&lt;h1&gt;
  
  
  Token Sprawl Is Real. Here's How to Cap It.
&lt;/h1&gt;

&lt;p&gt;Your engineering team's Anthropic bill jumped 4x last month. Nobody shipped a new product. Nobody onboarded a major customer. Someone just discovered they can pipe entire log files into Claude Opus to find a typo, and word got around. This is the conversation happening in finance meetings right now, and the fix isn't "ban AI" — it's plumbing.&lt;/p&gt;

&lt;p&gt;The tokenmaxxing phase — where every developer was encouraged to throw the biggest model at every task — was brief. We're now in the token rationing phase. CFOs are looking at usage dashboards that didn't exist 12 months ago and asking why a single engineer ran $1,800 of inference in a week to refactor a 200-line file. The companies handling this well aren't rationing access. They're routing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your AI spend exploded in 90 days
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent across the teams I audit: spend doesn't grow linearly with headcount or output. It grows with &lt;em&gt;habit formation&lt;/em&gt;. Once someone gets comfortable pasting a full repo into a frontier model, they stop pasting small chunks into smaller models. The unit cost per task doubles, then doubles again, while the perceived value stays flat.&lt;/p&gt;

&lt;p&gt;Three behaviors drive most of the bleed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontier-by-default.&lt;/strong&gt; Every task — summarizing a Slack thread, writing a regex, naming a variable — gets routed to the most expensive model available. Anthropic's own pricing pages show a roughly 5x cost gap between Haiku-class and Opus-class models per million tokens, and that gap matters when you multiply by 50 engineers making 200 calls a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No context discipline.&lt;/strong&gt; Engineers paste 80k tokens of context to answer a question that needs 2k. Long-context windows trained people to stop curating inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No caching.&lt;/strong&gt; The same system prompt — your coding standards, your architecture doc, your style guide — gets re-billed on every single request because nobody flipped on prompt caching.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;According to a 2025 a16z analysis of enterprise AI spend, the median company underestimated its first-year LLM cost by roughly 2-3x, primarily because individual developer usage scaled faster than procurement modeled. The number isn't the point. The pattern is: bottom-up adoption + top-down pricing = surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four categories of token waste
&lt;/h2&gt;

&lt;p&gt;Before you cut anything, sort your usage into these four buckets. The fix is different for each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;th&gt;Typical % of spend&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model mismatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Opus answering "format this JSON"&lt;/td&gt;
&lt;td&gt;30-50%&lt;/td&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context bloat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pasting whole repos for one-file edits&lt;/td&gt;
&lt;td&gt;15-25%&lt;/td&gt;
&lt;td&gt;Retrieval + scoping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repeat context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same system prompt billed 10k times/day&lt;/td&gt;
&lt;td&gt;10-20%&lt;/td&gt;
&lt;td&gt;Prompt caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failed agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Loops that retry 8 times before giving up&lt;/td&gt;
&lt;td&gt;5-15%&lt;/td&gt;
&lt;td&gt;Budgets + guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams attack this with a Slack announcement: "Please use Haiku for simple tasks." That fails within a week because individual engineers don't know what counts as simple, and the cost of guessing wrong (a worse answer) feels personal while the savings feel abstract. You need the routing to happen &lt;em&gt;below&lt;/em&gt; the engineer, not above them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model routing: the highest-leverage fix
&lt;/h2&gt;

&lt;p&gt;A router sits between your application (or your developers' tooling) and the model APIs. It looks at the request, decides which model is appropriate, and forwards it. Done right, it cuts spend 40-70% with no quality drop on the easy 80% of tasks, and you keep frontier models for the hard 20%.&lt;/p&gt;

&lt;p&gt;Here's a minimal router that classifies by task complexity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Cheap classifier decides routing
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify_complexity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify this task as SIMPLE, MEDIUM, or HARD. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                       &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reply with one word only.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;MODEL_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SIMPLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify_complexity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MODEL_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally crude. A production router will use signals like: input token count, presence of code, whether the user is a paid customer, whether the request is part of an agent loop, and historical accuracy on similar tasks. Tools like OpenRouter, Martian, and Portkey solve the boring parts (failover, observability) so you can focus on the routing rules.&lt;/p&gt;

&lt;p&gt;A few rules of thumb that have held up across my client projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to the mid-tier model&lt;/strong&gt;, not the frontier one. Use the cheap model as the classifier, not the worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route by intent, not by user.&lt;/strong&gt; "All engineers use Sonnet" is a worse policy than "all single-file edits use Sonnet."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the routing decision.&lt;/strong&gt; You need to A/B the classifier itself when accuracy complaints come in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prompt caching is free money
&lt;/h2&gt;

&lt;p&gt;If you're sending the same system prompt with every request — your coding standards, your tool definitions, your few-shot examples — you're paying full price for tokens the provider has already processed for you minutes ago. Anthropic's prompt caching reduces the cost of cached input tokens to roughly 10% of the standard rate, with cache writes priced at about 1.25x. The break-even is usually 2-3 hits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LARGE_STATIC_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 8000 tokens of standards
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to cache:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System prompts over ~1k tokens&lt;/li&gt;
&lt;li&gt;Tool definitions for agents&lt;/li&gt;
&lt;li&gt;Long reference documents queried repeatedly (style guides, schemas, API specs)&lt;/li&gt;
&lt;li&gt;Conversation history in long-running sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What &lt;em&gt;not&lt;/em&gt; to cache: anything that changes per user, retrieval results, or short prompts where the cache write overhead eats the savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-sizing agents: the 5x problem
&lt;/h2&gt;

&lt;p&gt;Agents are where token bills go nonlinear. A poorly-built agent doesn't cost 2x a single call — it costs 10-50x, because each step adds the full conversation history back into context. A 12-step agent with growing history can re-bill the same opening tokens twelve times.&lt;/p&gt;

&lt;p&gt;Three controls that actually work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Hard budget per task.&lt;/strong&gt; Every agent invocation gets a token ceiling. When it's hit, the agent must return whatever it has or escalate to a human.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBudget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent exceeded budget: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In your agent loop
&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TokenBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Context window pruning.&lt;/strong&gt; After every N steps, summarize the conversation back down. Yes, you lose some fidelity. You also stop paying for tokens about a tool call you made eight steps ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tool-call short-circuits.&lt;/strong&gt; If the agent calls the same tool with the same arguments twice in a row, that's a loop. Kill it. Most agent frameworks now support this natively but very few teams configure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical FinOps stack for LLMs
&lt;/h2&gt;

&lt;p&gt;The companies handling token sprawl well treat it like any other infra cost: meter it, attribute it, alert on it. Here's the minimum viable stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Open-source options&lt;/th&gt;
&lt;th&gt;Commercial&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single endpoint, key management, routing&lt;/td&gt;
&lt;td&gt;LiteLLM, Helicone&lt;/td&gt;
&lt;td&gt;Portkey, OpenRouter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-request logs, cost, latency&lt;/td&gt;
&lt;td&gt;Langfuse, Phoenix&lt;/td&gt;
&lt;td&gt;Datadog LLM, Helicone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Semantic + exact match&lt;/td&gt;
&lt;td&gt;GPTCache, Redis&lt;/td&gt;
&lt;td&gt;Provider-native (Anthropic, OpenAI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Catch quality regressions when routing&lt;/td&gt;
&lt;td&gt;Promptfoo, DeepEval&lt;/td&gt;
&lt;td&gt;Braintrust, Patronus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't need all of this on day one. The order that has worked for me: gateway first (so you have one chokepoint), observability second (so you know what's actually happening), caching third (the easy wins), evals fourth (so you can route more aggressively without fear).&lt;/p&gt;

&lt;p&gt;A 2025 OpenAI cookbook recommendation puts it bluntly: "Optimize for the cheapest model that meets your quality bar, not the smartest model you have access to." That's the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policies that don't make engineers hate you
&lt;/h2&gt;

&lt;p&gt;Token rationing fails the moment it feels like surveillance. Here's what works in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget by team, not by person.&lt;/strong&gt; A team with a $5k/month inference budget will self-police. A person with a $200/month budget will hide usage on personal keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface cost in the IDE.&lt;/strong&gt; If an engineer sees "this prompt will cost ~$0.40" before sending, behavior shifts immediately. Cursor, Continue, and Claude Code all support this now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the cheap path the default path.&lt;/strong&gt; If using Haiku requires a config change and Opus is the default, you've lost. Flip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish a "when to use what" doc.&lt;/strong&gt; Three lines, not three pages: "Use Haiku for: extraction, formatting, classification. Sonnet for: code, drafts, reasoning. Opus for: anything where a wrong answer costs more than $50."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the savings publicly.&lt;/strong&gt; "Routing saved us $14k in May" goes in the engineering all-hands. People want to be on the team that ships the optimization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn't work: blanket bans, per-request approval workflows, weekly "AI cost" shame emails. Those just push usage to personal credit cards and Anthropic Console accounts you don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BizFlowAI approaches this
&lt;/h2&gt;

&lt;p&gt;Most of our client engagements that start as "build us an agent" end up including a token spend audit, because the bill is often the reason they called. We typically find a 40-60% reduction available within two weeks: model routing on the obvious traffic, prompt caching on the long system prompts, and tightening the agent loops that were spinning. The work is unglamorous — instrumenting the gateway, classifying historical traffic, swapping models behind a feature flag — but it's the difference between an AI line item that scales with revenue and one that scales with team morale.&lt;/p&gt;

&lt;p&gt;If your monthly AI bill has more than doubled in the last quarter and you can't draw a straight line from that increase to revenue or hours saved, that's the audit. We map every call to a category, route the easy 80% to right-sized models, set hard budgets on agent loops, and leave you with a dashboard that shows cost per task instead of cost per month. Book a discovery call and we'll walk through your actual usage before recommending anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;If you read this far, here's the order I'd attack it in starting Monday:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pull the last 30 days of usage&lt;/strong&gt; from your provider's console. Sort by user and by endpoint. The Pareto distribution will be brutal — usually 3-5 people account for 60%+ of spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable prompt caching&lt;/strong&gt; on your largest system prompts. This is the cheapest 20% win available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a gateway in front of everything.&lt;/strong&gt; Even if you don't route yet, having one chokepoint means you can route tomorrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set per-team budgets&lt;/strong&gt;, not per-person. Make them generous enough that nobody hits them in week one — you want data, not friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add cost-per-task to your engineering dashboards&lt;/strong&gt; alongside latency and error rate. Make it visible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The companies that handled cloud cost sprawl in 2014-2018 are the same ones handling token sprawl in 2026: the ones who built FinOps muscle early. You don't need to be Netflix about it. You do need a gateway, a routing rule, and someone who looks at the dashboard once a week.&lt;/p&gt;

&lt;p&gt;The era of "just throw the biggest model at it" is ending not because the models got worse, but because the bills got real. The teams that adapt fastest will be the ones that treat inference like compute — measured, attributed, and right-sized — instead of like magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Work with BizFlowAI
&lt;/h2&gt;

&lt;p&gt;If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://calendly.com/lamingsrb" rel="noopener noreferrer"&gt;Book a free discovery call&lt;/a&gt;&lt;/strong&gt; — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.&lt;/p&gt;

&lt;p&gt;More guides like this on the &lt;a href="https://dev.to/blog"&gt;BizFlowAI blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llmcostoptimization</category>
      <category>claudepromptcaching</category>
      <category>anthropicapipricing</category>
      <category>llmmodelrouting</category>
    </item>
  </channel>
</rss>
