<?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: Lars Winstand</title>
    <description>The latest articles on DEV Community by Lars Winstand (@lars_winstand).</description>
    <link>https://dev.to/lars_winstand</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%2F3908932%2Feb8bc1ff-405f-4ef0-8204-ba1ed7caa59f.jpeg</url>
      <title>DEV Community: Lars Winstand</title>
      <link>https://dev.to/lars_winstand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lars_winstand"/>
    <language>en</language>
    <item>
      <title>I finally figured out what people mean by the best uncensored LLM after watching 17 agent workflows fail</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:09:39 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-finally-figured-out-what-people-mean-by-the-best-uncensored-llm-after-watching-17-agent-workflows-242h</link>
      <guid>https://dev.to/lars_winstand/i-finally-figured-out-what-people-mean-by-the-best-uncensored-llm-after-watching-17-agent-workflows-242h</guid>
      <description>&lt;p&gt;A few months ago I noticed something weird about how teams ask for the “best uncensored LLM.”&lt;/p&gt;

&lt;p&gt;The request sounds dramatic.&lt;/p&gt;

&lt;p&gt;The actual production problem usually isn’t.&lt;/p&gt;

&lt;p&gt;Nobody on a serious team is saying, “I need a model that will do literally anything.”&lt;/p&gt;

&lt;p&gt;What they’re really saying is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;my security agent refused to summarize a phishing kit&lt;/li&gt;
&lt;li&gt;my moderation pipeline refused to classify explicit text&lt;/li&gt;
&lt;li&gt;my coding agent bailed when the prompt mentioned Bash, scraping, auth flows, or packet capture&lt;/li&gt;
&lt;li&gt;my n8n or Make automation got weirdly cautious right when it needed to do real ops work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a completely different problem.&lt;/p&gt;

&lt;p&gt;And once you frame it that way, a lot of bad model choices suddenly make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most teams asking for an uncensored model are really asking for fewer false refusals
&lt;/h2&gt;

&lt;p&gt;This is the quiet version of the conversation.&lt;/p&gt;

&lt;p&gt;If you run internal security workflows, you do not want your model moralizing when you need it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;summarize malware behavior&lt;/li&gt;
&lt;li&gt;extract IOCs from a phishing kit&lt;/li&gt;
&lt;li&gt;explain an exploit proof of concept&lt;/li&gt;
&lt;li&gt;compare two suspicious payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run moderation, the model has to actually read ugly content.&lt;/p&gt;

&lt;p&gt;If your pipeline is classifying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sexual content&lt;/li&gt;
&lt;li&gt;hate speech&lt;/li&gt;
&lt;li&gt;harassment&lt;/li&gt;
&lt;li&gt;violent threats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then refusing to process the raw text is not safety.&lt;/p&gt;

&lt;p&gt;It’s just failure with better branding.&lt;/p&gt;

&lt;p&gt;And coding agents hit this wall all the time.&lt;/p&gt;

&lt;p&gt;A normal internal automation might need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write a Playwright script&lt;/li&gt;
&lt;li&gt;generate a credential rotation job&lt;/li&gt;
&lt;li&gt;build a SQL migration helper&lt;/li&gt;
&lt;li&gt;parse packet captures&lt;/li&gt;
&lt;li&gt;inspect logs containing slurs or threats&lt;/li&gt;
&lt;li&gt;reproduce an auth bypass bug so engineering can patch it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is edgy.&lt;/p&gt;

&lt;p&gt;But some APIs get twitchy the second the prompt includes words like &lt;code&gt;bypass&lt;/code&gt;, &lt;code&gt;exploit&lt;/code&gt;, &lt;code&gt;scrape&lt;/code&gt;, or &lt;code&gt;shell&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So when developers search for an uncensored LLM API, what they often mean is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;please stop refusing legitimate internal work just because the text looks scary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workloads that actually need lower-refusal behavior
&lt;/h2&gt;

&lt;p&gt;Not every workflow needs this.&lt;/p&gt;

&lt;p&gt;Most don’t.&lt;/p&gt;

&lt;p&gt;But the ones that do really do.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Security triage and exploit reproduction
&lt;/h3&gt;

&lt;p&gt;This is the clearest case.&lt;/p&gt;

&lt;p&gt;A security team might need a model to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;summarize malware behavior&lt;/li&gt;
&lt;li&gt;extract domains, hashes, or indicators from a phishing kit&lt;/li&gt;
&lt;li&gt;explain an exploit PoC&lt;/li&gt;
&lt;li&gt;reproduce a bug internally so it can be patched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontier models like GPT-5 or Claude Opus can be excellent at reasoning.&lt;/p&gt;

&lt;p&gt;That’s not the issue.&lt;/p&gt;

&lt;p&gt;The issue is provider policy.&lt;/p&gt;

&lt;p&gt;If your workflow lives near security-sensitive content every day, a lower-refusal open-weight model can be more useful than a stronger closed model that keeps slamming the brakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Moderation and trust-and-safety pipelines
&lt;/h3&gt;

&lt;p&gt;This one sounds ironic because it is a safety use case.&lt;/p&gt;

&lt;p&gt;But it still benefits from lower-refusal behavior.&lt;/p&gt;

&lt;p&gt;If you need to classify explicit or abusive user-generated content at scale, over-refusal is counterproductive.&lt;/p&gt;

&lt;p&gt;The safer pattern is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;let the model read the ugly content&lt;/li&gt;
&lt;li&gt;keep the workflow contained&lt;/li&gt;
&lt;li&gt;add logging and downstream policy checks&lt;/li&gt;
&lt;li&gt;restrict tool access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model should process the mess.&lt;/p&gt;

&lt;p&gt;Your system should contain the risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Internal coding agents and automations
&lt;/h3&gt;

&lt;p&gt;This is where the “uncensored” label gets abused, but there is a real issue underneath it.&lt;/p&gt;

&lt;p&gt;An internal agent in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n&lt;/li&gt;
&lt;li&gt;Make&lt;/li&gt;
&lt;li&gt;Zapier&lt;/li&gt;
&lt;li&gt;OpenClaw&lt;/li&gt;
&lt;li&gt;a custom Python runner&lt;/li&gt;
&lt;li&gt;an internal Node service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;might need to generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash scripts&lt;/li&gt;
&lt;li&gt;scraping logic&lt;/li&gt;
&lt;li&gt;packet parsers&lt;/li&gt;
&lt;li&gt;credential rotation jobs&lt;/li&gt;
&lt;li&gt;debugging helpers for auth flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are normal tasks.&lt;/p&gt;

&lt;p&gt;If your model starts refusing every time shell access or security-sensitive APIs show up, your agent fails exactly where ops teams need it most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question is not “which model is least censored?”
&lt;/h2&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;which exact task is getting falsely refused?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question forces better decisions.&lt;/p&gt;

&lt;p&gt;Because sometimes the answer is “switch models.”&lt;/p&gt;

&lt;p&gt;And sometimes the answer is “your workflow is sloppy.”&lt;/p&gt;

&lt;h2&gt;
  
  
  API compatibility matters more than rebellious branding
&lt;/h2&gt;

&lt;p&gt;This is the part a lot of teams miss.&lt;/p&gt;

&lt;p&gt;Nobody wants to rebuild an agent stack just to test one model with fewer refusals.&lt;/p&gt;

&lt;p&gt;That’s why OpenAI-compatible APIs matter so much.&lt;/p&gt;

&lt;p&gt;If your app already speaks the OpenAI API shape, you can swap the backend without rewriting your whole system.&lt;/p&gt;

&lt;p&gt;That is why tools like &lt;code&gt;vLLM&lt;/code&gt; are so useful for this category.&lt;/p&gt;

&lt;p&gt;You can serve open-weight models behind an OpenAI-compatible endpoint and test them in your real workflow instead of arguing in Slack about screenshots and benchmarks.&lt;/p&gt;

&lt;p&gt;A minimal example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve meta-llama/Llama-3.1-70B-Instruct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your existing client at the new base URL:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your app code may barely change:&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;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/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;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dummy&lt;/span&gt;&lt;span class="sh"&gt;"&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;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&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;meta-llama/Llama-3.1-70B-Instruct&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="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;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;system&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a security analysis assistant.&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this phishing kit and extract indicators.&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="nf"&gt;print&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;choices&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;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That kind of swap is a big deal.&lt;/p&gt;

&lt;p&gt;Because if your n8n flow or Python worker can point at a different &lt;code&gt;base_url&lt;/code&gt;, you can test behavior inside the real automation.&lt;/p&gt;

&lt;p&gt;That beats theoretical debate every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 practical architecture options
&lt;/h2&gt;

&lt;p&gt;Here’s the cleanest way I’ve found to think about it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What you actually get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open-weight model via vLLM&lt;/td&gt;
&lt;td&gt;Lower refusal potential depending on model choice, OpenAI-compatible serving, but you have to self-host and build your own guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closed API like OpenAI, Anthropic, or xAI&lt;/td&gt;
&lt;td&gt;Strong reasoning and coding quality, managed infrastructure, but provider-enforced policies limit flexibility on sensitive workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router layer with an OpenAI-compatible endpoint&lt;/td&gt;
&lt;td&gt;Easy model swapping and workload-based routing, but still constrained by provider policy unless you route to open or self-hosted models&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My opinion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if your problem is occasional false refusals in a mostly normal coding workflow, routing between GPT-5, Claude Opus 4.6, and Grok 4.20 is often enough&lt;/li&gt;
&lt;li&gt;if your problem is persistent refusal on malware analysis, exploit reproduction, or explicit-content moderation, you probably need an open-weight route somewhere in the stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not ideology.&lt;/p&gt;

&lt;p&gt;That’s just operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lot of teams do not need an uncensored model. They need better workflow design.
&lt;/h2&gt;

&lt;p&gt;This is the part people hate hearing.&lt;/p&gt;

&lt;p&gt;I’ve seen teams blame the model when the actual issue was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vague prompts&lt;/li&gt;
&lt;li&gt;junk retrieval context&lt;/li&gt;
&lt;li&gt;broad tool permissions&lt;/li&gt;
&lt;li&gt;no separation between analysis and action&lt;/li&gt;
&lt;li&gt;no logging before side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, switching to a looser model just gives you a more confident failure mode.&lt;/p&gt;

&lt;p&gt;A boring workflow cleanup often fixes more than people expect.&lt;/p&gt;

&lt;p&gt;Here’s the checklist I usually recommend first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;tighten the system prompt&lt;/li&gt;
&lt;li&gt;separate classification from action-taking&lt;/li&gt;
&lt;li&gt;restrict tool permissions by task&lt;/li&gt;
&lt;li&gt;scope retrieval to only relevant docs&lt;/li&gt;
&lt;li&gt;log model decisions before external side effects&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A quick example.&lt;/p&gt;

&lt;p&gt;Bad pattern:&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;# One agent can read tickets, write scripts, execute shell commands,
# and call external APIs with almost no boundaries.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better pattern:&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;# Agent 1: classify issue
# Agent 2: propose remediation steps
# Agent 3: execute only approved, scoped actions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split alone removes a surprising amount of chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lower refusal does not automatically mean better for production
&lt;/h2&gt;

&lt;p&gt;This is where the “uncensored” conversation usually gets unserious.&lt;/p&gt;

&lt;p&gt;Lower refusal rates are useful for some workloads.&lt;/p&gt;

&lt;p&gt;They are not automatically good.&lt;/p&gt;

&lt;p&gt;If your agent touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer data&lt;/li&gt;
&lt;li&gt;regulated workflows&lt;/li&gt;
&lt;li&gt;autonomous tools&lt;/li&gt;
&lt;li&gt;production systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then stricter provider behavior may be a feature, not a bug.&lt;/p&gt;

&lt;p&gt;A model that willingly helps with exploit reproduction in a security lab may also be more willing to generate dangerous instructions in the wrong context.&lt;/p&gt;

&lt;p&gt;A moderation model that reliably processes explicit content may also mishandle regulated material if your downstream checks are weak.&lt;/p&gt;

&lt;p&gt;So the real evaluation questions are:&lt;/p&gt;

&lt;h3&gt;
  
  
  Which workload needs lower refusal?
&lt;/h3&gt;

&lt;p&gt;Be specific.&lt;/p&gt;

&lt;p&gt;Security triage is not customer support.&lt;/p&gt;

&lt;p&gt;Malware analysis is not a healthcare assistant.&lt;/p&gt;

&lt;h3&gt;
  
  
  What containment do you have?
&lt;/h3&gt;

&lt;p&gt;You want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;approval gates&lt;/li&gt;
&lt;li&gt;read-only tools where possible&lt;/li&gt;
&lt;li&gt;scoped retrieval&lt;/li&gt;
&lt;li&gt;audit trails&lt;/li&gt;
&lt;li&gt;post-generation policy checks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Can you swap models without rewriting everything?
&lt;/h3&gt;

&lt;p&gt;This is why OpenAI-compatible endpoints matter so much.&lt;/p&gt;

&lt;p&gt;If you can route by workload, you don’t need one model to be perfect at everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup I keep seeing work
&lt;/h2&gt;

&lt;p&gt;The smartest teams are usually not betting everything on one “uncensored” model.&lt;/p&gt;

&lt;p&gt;They do something much more boring and much more effective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use stricter closed models where policy alignment is helpful&lt;/li&gt;
&lt;li&gt;use lower-refusal open models only where legitimate internal work keeps getting blocked&lt;/li&gt;
&lt;li&gt;keep everything behind the same API shape so routing is easy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the grown-up version.&lt;/p&gt;

&lt;p&gt;And honestly, it’s also the only version that survives contact with production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Standard Compute fits
&lt;/h2&gt;

&lt;p&gt;This is exactly why I like OpenAI-compatible router layers for agent workloads.&lt;/p&gt;

&lt;p&gt;If your team is already running automations in n8n, Make, Zapier, OpenClaw, or custom code, the best setup is usually the one that lets you swap models without rewriting clients.&lt;/p&gt;

&lt;p&gt;That’s the useful part of Standard Compute.&lt;/p&gt;

&lt;p&gt;It gives you an OpenAI-compatible endpoint with flat monthly pricing, so you can route workloads across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20 without doing pricing math every time an agent loops harder than expected.&lt;/p&gt;

&lt;p&gt;That matters a lot for automations.&lt;/p&gt;

&lt;p&gt;Especially when your real problem is not just refusals.&lt;/p&gt;

&lt;p&gt;It’s also cost predictability.&lt;/p&gt;

&lt;p&gt;Per-token billing makes teams weirdly conservative with agents. They shorten prompts too aggressively, avoid useful retries, and babysit workflows that should just run.&lt;/p&gt;

&lt;p&gt;Flat-rate compute changes that behavior.&lt;/p&gt;

&lt;p&gt;If you’re building agents that run all day, that pricing model is often as important as model quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  My practical takeaway
&lt;/h2&gt;

&lt;p&gt;If you’re searching for the best uncensored LLM, stop asking which model has the loudest anti-censorship branding.&lt;/p&gt;

&lt;p&gt;Ask which exact task is failing.&lt;/p&gt;

&lt;p&gt;If it’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser automation&lt;/li&gt;
&lt;li&gt;SQL migrations&lt;/li&gt;
&lt;li&gt;packet parsing&lt;/li&gt;
&lt;li&gt;internal shell scripting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then fix prompts and permissions first.&lt;/p&gt;

&lt;p&gt;If it’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;malware triage&lt;/li&gt;
&lt;li&gt;exploit reproduction for patching&lt;/li&gt;
&lt;li&gt;explicit-content classification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then yes, a lower-refusal open-weight model served through something like &lt;code&gt;vLLM&lt;/code&gt; may be the right answer.&lt;/p&gt;

&lt;p&gt;Just don’t confuse “less likely to refuse” with “better for production.”&lt;/p&gt;

&lt;p&gt;The winning setup is usually the one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fits your existing agent stack&lt;/li&gt;
&lt;li&gt;speaks OpenAI-compatible API&lt;/li&gt;
&lt;li&gt;handles the ugly parts of your workload&lt;/li&gt;
&lt;li&gt;gives you enough guardrails that Monday morning doesn’t start with an incident review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a much less exciting answer than “find the most uncensored model.”&lt;/p&gt;

&lt;p&gt;It’s also the answer that actually works.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>My AI cron job looked cheap at 5 runs/day. At 96 runs/day it turned into 1,728 model calls</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:08:25 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-ai-cron-job-looked-cheap-at-5-runsday-at-96-runsday-it-turned-into-1728-model-calls-4023</link>
      <guid>https://dev.to/lars_winstand/my-ai-cron-job-looked-cheap-at-5-runsday-at-96-runsday-it-turned-into-1728-model-calls-4023</guid>
      <description>&lt;p&gt;I learned this the annoying way: the expensive part of an AI cron job usually isn't the prompt.&lt;/p&gt;

&lt;p&gt;It's the production behavior around it.&lt;/p&gt;

&lt;p&gt;A workflow that looks basically free in testing can turn into a small army of billable model calls once you add retries, polling, multi-step chains, and separate environments.&lt;/p&gt;

&lt;p&gt;I had an n8n cron running every 15 minutes. In testing, it seemed harmless. Then I turned on retries, added a Claude Opus 4.6 summary step after a GPT-5.4 extraction step, mirrored the workflow in staging and prod, and suddenly my neat little automation had very different economics.&lt;/p&gt;

&lt;p&gt;The prompt wasn't the problem.&lt;/p&gt;

&lt;p&gt;The architecture was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The testing version lied
&lt;/h2&gt;

&lt;p&gt;My original mental model looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron -&amp;gt; fetch data -&amp;gt; call model -&amp;gt; done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in testing, that was kind of true.&lt;/p&gt;

&lt;p&gt;I ran it manually a few times. Maybe let the cron fire 5 times in a day. Cost looked tiny. Everyone moved on.&lt;/p&gt;

&lt;p&gt;Production looked more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron -&amp;gt; fetch records -&amp;gt; GPT-5.4 classify -&amp;gt; Claude Opus 4.6 summarize
     -&amp;gt; retry on timeout -&amp;gt; poll downstream status -&amp;gt; send output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then there were three copies of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dev
staging
prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is how "one workflow" becomes a swarm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math that changed the conversation
&lt;/h2&gt;

&lt;p&gt;Here's the simple version.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Calls per day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Testing assumption: 5 runs/day × 1 model call&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real production schedule: 96 runs/day × 3 model calls&lt;/td&gt;
&lt;td&gt;288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add 2 retries across the workflow&lt;/td&gt;
&lt;td&gt;576&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiply across dev, staging, and prod&lt;/td&gt;
&lt;td&gt;1,728&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 1,728 number is not some nightmare edge case.&lt;/p&gt;

&lt;p&gt;It's very normal if you build scheduled automations the way they're actually supposed to be built.&lt;/p&gt;

&lt;p&gt;Retries are normal.&lt;br&gt;
Polling is normal.&lt;br&gt;
Separate environments are normal.&lt;br&gt;
Multi-step pipelines are normal.&lt;/p&gt;

&lt;p&gt;The spreadsheet was wrong because it modeled the happy path, not the real system.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why AI cron jobs blow up in production
&lt;/h2&gt;

&lt;p&gt;Because production is where all the reliability features show up.&lt;/p&gt;

&lt;p&gt;Here's the kind of stack that quietly multiplies usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n cron trigger every 15 minutes&lt;/li&gt;
&lt;li&gt;webhook or HTTP step to pull new records&lt;/li&gt;
&lt;li&gt;GPT-5.4 classification call&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 summary call&lt;/li&gt;
&lt;li&gt;retry queue when the upstream API times out&lt;/li&gt;
&lt;li&gt;polling step to wait for downstream completion&lt;/li&gt;
&lt;li&gt;separate dev, staging, and prod environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is weird.&lt;/p&gt;

&lt;p&gt;That's just what happens when a workflow graduates from demo to useful.&lt;/p&gt;

&lt;p&gt;If you're running Make scenarios, Zapier schedules, OpenClaw agents, or custom Python workers on cron, the same pattern shows up fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  A quick way to estimate the real cost
&lt;/h2&gt;

&lt;p&gt;If you're still estimating AI automation cost with "how much does one prompt cost?", you're probably undercounting by a lot.&lt;/p&gt;

&lt;p&gt;A better back-of-the-napkin formula is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total_daily_calls = scheduled_runs_per_day
                  × model_calls_per_run
                  × retry_multiplier
                  × environment_count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;96 runs/day
× 3 model calls/run
× 2 retry multiplier
× 3 environments
= 1,728 calls/day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to make this concrete in code:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateDailyCalls&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;runsPerDay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;modelCallsPerRun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;retryMultiplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;environments&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;return&lt;/span&gt; &lt;span class="nx"&gt;runsPerDay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;modelCallsPerRun&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;retryMultiplier&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;environments&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;dailyCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateDailyCalls&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;runsPerDay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelCallsPerRun&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="na"&gt;retryMultiplier&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="na"&gt;environments&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="nx"&gt;console&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="nx"&gt;dailyCalls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1728&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you're more shell-script-brained:&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="nv"&gt;runs_per_day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;96
&lt;span class="nv"&gt;model_calls_per_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;span class="nv"&gt;retry_multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;span class="nv"&gt;environments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;runs_per_day &lt;span class="o"&gt;*&lt;/span&gt; model_calls_per_run &lt;span class="o"&gt;*&lt;/span&gt; retry_multiplier &lt;span class="o"&gt;*&lt;/span&gt; environments&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="c"&gt;# 1728&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This still won't be perfect, but it's a lot closer to reality than pricing one prompt in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expensive part is usually not the first prompt
&lt;/h2&gt;

&lt;p&gt;This is the part I think a lot of teams miss.&lt;/p&gt;

&lt;p&gt;People spend hours trying to shave 8% off a prompt while ignoring the bigger multiplier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one model call becomes three&lt;/li&gt;
&lt;li&gt;one run becomes 96 runs/day&lt;/li&gt;
&lt;li&gt;one clean execution becomes retries + polling&lt;/li&gt;
&lt;li&gt;one environment becomes dev + staging + prod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen workflows where GPT-5.4 does extraction, Claude Opus 4.6 does summarization, and Grok 4.20 gets pulled in for a second pass or rewrite.&lt;/p&gt;

&lt;p&gt;That can absolutely improve quality.&lt;/p&gt;

&lt;p&gt;It also means your "simple automation" is now several billable inference steps deep before the retry queue even wakes up.&lt;/p&gt;

&lt;p&gt;That's not bad engineering. Sometimes it's the right architecture.&lt;/p&gt;

&lt;p&gt;But it does mean usage-based pricing gets painful fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem with per-token pricing for scheduled workflows
&lt;/h2&gt;

&lt;p&gt;Per-token pricing is fine when you're experimenting.&lt;/p&gt;

&lt;p&gt;It's much worse when you're running always-on agents and scheduled automations that are supposed to be boring and reliable.&lt;/p&gt;

&lt;p&gt;Because every sensible production improvement makes cost forecasting worse.&lt;/p&gt;

&lt;p&gt;Add retries? More spend.&lt;/p&gt;

&lt;p&gt;Add staging? More spend.&lt;/p&gt;

&lt;p&gt;Increase polling because a partner API is flaky? More spend.&lt;/p&gt;

&lt;p&gt;Split one prompt into extraction + summarization because output quality improved? More spend.&lt;/p&gt;

&lt;p&gt;That is a bad incentive structure.&lt;/p&gt;

&lt;p&gt;Better engineering should not make the bill harder to predict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;The shift for me was treating AI automation like infrastructure, not like a one-off API experiment.&lt;/p&gt;

&lt;p&gt;That changed how I thought about pricing.&lt;/p&gt;

&lt;p&gt;For cron-heavy workflows, flat-rate compute is just a better fit than per-token billing.&lt;/p&gt;

&lt;p&gt;If you're running n8n, Make, Zapier, OpenClaw, or custom cron jobs through an OpenAI-compatible API, you want the reliability features turned on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;polling&lt;/li&gt;
&lt;li&gt;background processing&lt;/li&gt;
&lt;li&gt;multiple environments&lt;/li&gt;
&lt;li&gt;multi-step model chains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not want engineers doing token math every week because the workflow is finally behaving like production software.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;If your code already talks to an OpenAI-compatible API, swapping providers should not require a rewrite.&lt;/p&gt;

&lt;p&gt;Example with the OpenAI SDK pattern:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;STANDARD_COMPUTE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.standardcompute.com/v1&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Classify incoming support tickets.&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment failed after checkout&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="nx"&gt;console&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workflow is in n8n, Make, Zapier, or a custom worker, the point is the same: keep the workflow architecture you need, without turning reliability into a budgeting problem.&lt;/p&gt;

&lt;p&gt;Standard Compute is built for exactly this kind of workload: AI agents, automations, scheduled jobs, and OpenAI-compatible integrations that need predictable monthly cost instead of surprise usage bills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checks before you ship an AI cron job
&lt;/h2&gt;

&lt;p&gt;If I were reviewing one of these workflows now, I'd ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How many scheduled runs happen per day in production?&lt;/li&gt;
&lt;li&gt;How many model calls happen per run, including summary/rewrite/classification steps?&lt;/li&gt;
&lt;li&gt;What is the retry behavior under partial failure?&lt;/li&gt;
&lt;li&gt;Are there polling loops?&lt;/li&gt;
&lt;li&gt;How many environments are active?&lt;/li&gt;
&lt;li&gt;Are there hidden fan-out steps for batches or records?&lt;/li&gt;
&lt;li&gt;Does the pricing model still make sense after all of that?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't answer those, the workflow is probably more expensive than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  My actual takeaway
&lt;/h2&gt;

&lt;p&gt;Testing lies by omission.&lt;/p&gt;

&lt;p&gt;Production tells the truth.&lt;/p&gt;

&lt;p&gt;And the truth is that always-on AI automation does not stay cheap just because the first five runs were cheap.&lt;/p&gt;

&lt;p&gt;If your workflow is scheduled, retried, polled, duplicated across environments, and expected to run 24/7 without babysitting, then the billing model matters as much as the prompt.&lt;/p&gt;

&lt;p&gt;That was the part I missed at first.&lt;/p&gt;

&lt;p&gt;If you're building cron-heavy AI workflows and you're tired of per-token pricing turning normal engineering into a finance problem, Standard Compute is worth a look: &lt;a href="https://standardcompute.com" rel="noopener noreferrer"&gt;https://standardcompute.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>n8n</category>
    </item>
    <item>
      <title>My AI agent self-healing fix was embarrassingly simple once I stopped retrying the whole thing</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sun, 20 Sep 2026 22:08:43 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-ai-agent-self-healing-fix-was-embarrassingly-simple-once-i-stopped-retrying-the-whole-thing-3a42</link>
      <guid>https://dev.to/lars_winstand/my-ai-agent-self-healing-fix-was-embarrassingly-simple-once-i-stopped-retrying-the-whole-thing-3a42</guid>
      <description>&lt;h1&gt;
  
  
  My AI agent self-healing fix was embarrassingly simple once I stopped retrying the whole thing
&lt;/h1&gt;

&lt;p&gt;I finally snapped after watching an agent die on step 8 of 12, then restart from step 1 like it had learned absolutely nothing.&lt;/p&gt;

&lt;p&gt;The workflow was not exotic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pull leads from a form&lt;/li&gt;
&lt;li&gt;enrich them&lt;/li&gt;
&lt;li&gt;summarize the account&lt;/li&gt;
&lt;li&gt;draft outreach&lt;/li&gt;
&lt;li&gt;push results into a CRM&lt;/li&gt;
&lt;li&gt;notify the team in Slack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure was dumb: one HTTP timeout during enrichment.&lt;/p&gt;

&lt;p&gt;But because I had wired the workflow to retry the entire run, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;re-did earlier model calls&lt;/li&gt;
&lt;li&gt;re-fetched records I already had&lt;/li&gt;
&lt;li&gt;almost created duplicate CRM updates&lt;/li&gt;
&lt;li&gt;burned compute on work that had already succeeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In n8n, that meant another full execution.&lt;/p&gt;

&lt;p&gt;In LangGraph, that meant replay unless I handled state correctly.&lt;/p&gt;

&lt;p&gt;Either way, I was paying for my own bad design.&lt;/p&gt;

&lt;p&gt;I thought the answer would be better prompts.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;The answer was teaching the agent how to resume.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best ai agent self-healing pattern is not a smarter prompt. It is durable execution: save explicit state, retry only failure-prone steps, and resume with a stable execution ID.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are building long-running agents with n8n, LangGraph, Temporal, Make, Zapier, or custom workers, this is the line between "kind of works" and "survives production."&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem with full-workflow retries
&lt;/h2&gt;

&lt;p&gt;Full-workflow retries feel safe when the workflow is tiny.&lt;/p&gt;

&lt;p&gt;They become a disaster when your agent spans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple LLM calls&lt;/li&gt;
&lt;li&gt;external APIs&lt;/li&gt;
&lt;li&gt;human approvals&lt;/li&gt;
&lt;li&gt;writes into systems that really do not like duplicates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My original logic was painfully common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&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;runWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;runWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;That is not resilience.&lt;/p&gt;

&lt;p&gt;That is replay.&lt;/p&gt;

&lt;p&gt;And replay causes a bunch of avoidable problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;successful LLM calls get repeated&lt;/li&gt;
&lt;li&gt;tool calls hit external systems again&lt;/li&gt;
&lt;li&gt;token usage and latency go up for no reason&lt;/li&gt;
&lt;li&gt;logs get noisier on every retry&lt;/li&gt;
&lt;li&gt;one flaky step turns into a full pipeline failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Clearbit times out, or Salesforce rate-limits you, or a webhook returns 502, a better system prompt does not help.&lt;/p&gt;

&lt;p&gt;This is why I stopped asking:&lt;/p&gt;

&lt;p&gt;"How do I make the model less fragile?"&lt;/p&gt;

&lt;p&gt;And started asking:&lt;/p&gt;

&lt;p&gt;"What already succeeded, and how do I avoid doing it again?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: stable execution IDs + explicit state + step-level retries
&lt;/h2&gt;

&lt;p&gt;What finally worked was boring in the best way.&lt;/p&gt;

&lt;p&gt;I gave every run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a stable &lt;code&gt;execution_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;explicit persisted state after each meaningful step&lt;/li&gt;
&lt;li&gt;hard retry boundaries around the steps that actually fail in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state shape was not fancy. It looked like normal application plumbing.&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;"execution_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead_9f3d7c2a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_step"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summarize_account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lead_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"artifacts"&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;"enrichment"&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;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"employees"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&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;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B2B SaaS company expanding sales ops"&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;"side_effects"&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;"crm_upserted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slack_notified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"idempotency_keys"&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;"crm_write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm:lead_123:v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slack_post"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack:lead_123:v1"&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;"last_error"&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;"step"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enrichment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP timeout"&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 one change killed most of the chaos.&lt;/p&gt;

&lt;p&gt;The new rule became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry model or HTTP calls at the step level&lt;/li&gt;
&lt;li&gt;never repeat a side effect unless the step is idempotent&lt;/li&gt;
&lt;li&gt;resume from the last completed checkpoint using the same execution ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what self-healing looked like in practice.&lt;/p&gt;

&lt;p&gt;Not the agent becoming smarter.&lt;/p&gt;

&lt;p&gt;The agent becoming less forgetful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What step-level retry logic actually looks like
&lt;/h2&gt;

&lt;p&gt;Here is the pattern in plain TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;currentStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;sideEffects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;crmUpserted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
    &lt;span class="na"&gt;slackNotified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runLeadWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enrichment&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;retryStep&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;enrichLead&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enriched&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enriched&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&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;retryStep&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;summarizeAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enrichment&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summarized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summarized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sideEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crmUpserted&lt;/span&gt;&lt;span class="p"&gt;)&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;upsertCRM&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`crm:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sideEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crmUpserted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm_written&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm_written&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sideEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slackNotified&lt;/span&gt;&lt;span class="p"&gt;)&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;postToSlack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lead processed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`slack:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sideEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slackNotified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retryStep&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&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="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="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;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;lastError&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the syntax.&lt;/p&gt;

&lt;p&gt;It is the boundary.&lt;/p&gt;

&lt;p&gt;Each expensive or failure-prone step gets its own retry policy.&lt;/p&gt;

&lt;p&gt;Each successful step gets checkpointed.&lt;/p&gt;

&lt;p&gt;External writes are protected with idempotency keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resume, don’t restart
&lt;/h2&gt;

&lt;p&gt;The key mental shift is this:&lt;/p&gt;

&lt;p&gt;Separate workflow state from step execution.&lt;/p&gt;

&lt;p&gt;Once I did that, the tooling got much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  LangGraph: reuse &lt;code&gt;thread_id&lt;/code&gt; and persist graph state
&lt;/h2&gt;

&lt;p&gt;LangGraph gets a lot better once you stop treating every run like a fresh conversation.&lt;/p&gt;

&lt;p&gt;If you use a checkpointer and keep the same &lt;code&gt;thread_id&lt;/code&gt;, you can resume from existing state instead of replaying the graph from the top.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;

&lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemorySaver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&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;configurable&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;thread_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;lead-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&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_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;lead-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# later, after failure or human review
&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resume&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;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you pause with &lt;code&gt;interrupt()&lt;/code&gt; for human review, you can continue with &lt;code&gt;Command(resume=...)&lt;/code&gt; instead of rerunning the whole graph.&lt;/p&gt;

&lt;p&gt;That is a much better model for real automations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal: retry Activities, not the whole business process
&lt;/h2&gt;

&lt;p&gt;Temporal is probably the cleanest expression of this pattern.&lt;/p&gt;

&lt;p&gt;The Workflow keeps deterministic state.&lt;/p&gt;

&lt;p&gt;Activities do the flaky work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP calls&lt;/li&gt;
&lt;li&gt;database writes&lt;/li&gt;
&lt;li&gt;LLM tool invocations&lt;/li&gt;
&lt;li&gt;queue operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split matters.&lt;/p&gt;

&lt;p&gt;If the enrichment API times out, retry the enrichment Activity.&lt;/p&gt;

&lt;p&gt;Do not rerun the entire lead-processing workflow.&lt;/p&gt;

&lt;p&gt;Pseudo-example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// workflow.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;leadWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LeadInput&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;enrichment&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;enrichLeadActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;summary&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;summarizeLeadActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enrichment&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;upsertCrmActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&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;notifySlackActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&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;Then attach retry policy where failure actually happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxyActivities&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./activities&lt;/span&gt;&lt;span class="dl"&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="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;startToCloseTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2 minutes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maximumAttempts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole point.&lt;/p&gt;

&lt;p&gt;Retry the unstable edge, not the entire process.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n: manual, but still worth doing
&lt;/h2&gt;

&lt;p&gt;n8n is great for orchestration.&lt;/p&gt;

&lt;p&gt;But if you want resumability, you have to design it intentionally.&lt;/p&gt;

&lt;p&gt;Useful pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;execution.retryOf&lt;/code&gt; to detect retries&lt;/li&gt;
&lt;li&gt;Error Trigger for remediation flows&lt;/li&gt;
&lt;li&gt;external state in Postgres, Redis, Airtable, or another store keyed by execution ID&lt;/li&gt;
&lt;li&gt;branching logic to skip completed steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical pattern in n8n is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create an &lt;code&gt;execution_id&lt;/code&gt; at the start&lt;/li&gt;
&lt;li&gt;save progress after each major step&lt;/li&gt;
&lt;li&gt;on retry, load prior state&lt;/li&gt;
&lt;li&gt;skip steps that already completed&lt;/li&gt;
&lt;li&gt;only rerun the failed node or subflow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you something much closer to continuation instead of "start over and hope."&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison: restart vs resume
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What happens after step 8 fails?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full workflow retry&lt;/td&gt;
&lt;td&gt;Steps 1-7 run again, model calls repeat, external writes may duplicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step-level retry + checkpoints&lt;/td&gt;
&lt;td&gt;Only step 8 retries, prior outputs are reused, external writes stay protected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The cost problem is not subtle
&lt;/h2&gt;

&lt;p&gt;This part matters a lot if you run agents all day.&lt;/p&gt;

&lt;p&gt;Every full replay multiplies model calls.&lt;/p&gt;

&lt;p&gt;If your workflow has six LLM steps and one API timeout near the end, a naive retry can turn one failure into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;six extra inference calls&lt;/li&gt;
&lt;li&gt;repeated tool work&lt;/li&gt;
&lt;li&gt;more queue time&lt;/li&gt;
&lt;li&gt;more logs to untangle&lt;/li&gt;
&lt;li&gt;more chances to duplicate side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under per-token pricing, this gets expensive fast.&lt;/p&gt;

&lt;p&gt;And even if you are on flat-rate AI compute, wasted retries still hurt throughput.&lt;/p&gt;

&lt;p&gt;You are burning capacity on work that already succeeded.&lt;/p&gt;

&lt;p&gt;That is one reason I care about this pattern so much.&lt;/p&gt;

&lt;p&gt;If you are running agents in n8n, Make, Zapier, OpenClaw, or custom frameworks, step-level retries are one of the fastest ways to make the system both cheaper and less annoying.&lt;/p&gt;

&lt;p&gt;And if your stack still points at the OpenAI API with usage anxiety hanging over every replay, it is worth looking at a drop-in alternative like Standard Compute.&lt;/p&gt;

&lt;p&gt;Standard Compute gives you unlimited AI compute for a flat monthly price and works with existing OpenAI-compatible SDKs and HTTP clients. So when you fix your retry design, you are not also stuck babysitting per-token costs every time an automation gets noisy.&lt;/p&gt;

&lt;p&gt;That does not replace durable execution.&lt;/p&gt;

&lt;p&gt;It just means your cost model stops fighting your architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern I would use again from day one
&lt;/h2&gt;

&lt;p&gt;If I were rebuilding that workflow today, I would do this immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assign a stable execution ID at the start.&lt;/li&gt;
&lt;li&gt;Persist state after every expensive or meaningful step.&lt;/li&gt;
&lt;li&gt;Store outputs in a structured state object, not scattered logs.&lt;/li&gt;
&lt;li&gt;Put idempotency keys on every external write.&lt;/li&gt;
&lt;li&gt;Retry only failure-prone steps like LLM calls, HTTP requests, and queue operations.&lt;/li&gt;
&lt;li&gt;Resume from the last checkpoint instead of replaying the workflow.&lt;/li&gt;
&lt;li&gt;Send true failures to a human or remediation flow instead of blindly looping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want a minimal checklist for production agents, use this:&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;# production sanity checklist&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; stable execution &lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; persisted checkpoint after each major step
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; idempotency key on every external write
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; step-level retry policy
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; resume path tested
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; duplicate-write protection tested
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; human escalation path &lt;span class="k"&gt;for &lt;/span&gt;hard failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My actual takeaway
&lt;/h2&gt;

&lt;p&gt;My agent did not need a more inspirational prompt.&lt;/p&gt;

&lt;p&gt;It needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;boundaries&lt;/li&gt;
&lt;li&gt;permission to continue where it left off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I stopped retrying the whole thing, the self-healing part was embarrassingly simple.&lt;/p&gt;

&lt;p&gt;If your agent keeps "recovering" by replaying everything, it is not self-healing.&lt;/p&gt;

&lt;p&gt;It is just forgetting more aggressively.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>n8n</category>
      <category>devops</category>
    </item>
    <item>
      <title>My refund agent looked smart until it lied about Shopify, Stripe, and Zendesk 3 different ways</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sun, 20 Sep 2026 14:16:12 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-refund-agent-looked-smart-until-it-lied-about-shopify-stripe-and-zendesk-3-different-ways-42e2</link>
      <guid>https://dev.to/lars_winstand/my-refund-agent-looked-smart-until-it-lied-about-shopify-stripe-and-zendesk-3-different-ways-42e2</guid>
      <description>&lt;p&gt;I thought I had a prompt problem.&lt;/p&gt;

&lt;p&gt;I didn’t.&lt;/p&gt;

&lt;p&gt;I had a state problem.&lt;/p&gt;

&lt;p&gt;My support agent could read a Zendesk ticket, find the Shopify order, issue a Stripe refund, update the ticket, and reply to the customer. In staging, it looked great. In production, it started doing something much worse than "being wrong."&lt;/p&gt;

&lt;p&gt;It was confidently telling customers their refund was complete when the workflow had only fired an API call and hoped for the best.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Because once an LLM can call real systems like Shopify, Stripe, and Zendesk, the failure mode changes. The model doesn’t need to hallucinate a tool name to hurt you. It just needs to overstate what happened after a valid tool call.&lt;/p&gt;

&lt;p&gt;That’s the trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real bug: valid tool calls, invalid conclusions
&lt;/h2&gt;

&lt;p&gt;A lot of teams hit an "agent failed a task" moment and immediately do one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tighten the prompt&lt;/li&gt;
&lt;li&gt;add more warnings&lt;/li&gt;
&lt;li&gt;enforce stricter JSON schemas&lt;/li&gt;
&lt;li&gt;switch models&lt;/li&gt;
&lt;li&gt;add more examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes that helps.&lt;/p&gt;

&lt;p&gt;But if your agent can already call the right API with the right arguments, prompt work stops being the main lever.&lt;/p&gt;

&lt;p&gt;OpenAI’s Structured Outputs are a real improvement here. Their published evals showed &lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt; hitting 100% schema adherence on complex JSON schemas, versus less than 40% for &lt;code&gt;gpt-4-0613&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s great.&lt;/p&gt;

&lt;p&gt;But schema adherence is not operational truth.&lt;/p&gt;

&lt;p&gt;You can have perfect JSON and still ship a support workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reports success before money moved&lt;/li&gt;
&lt;li&gt;retries without preserving certainty&lt;/li&gt;
&lt;li&gt;drifts out of sync with async systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s what bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Shopify: a Refund object is not proof that money moved
&lt;/h2&gt;

&lt;p&gt;This was my first clue.&lt;/p&gt;

&lt;p&gt;The agent called Shopify &lt;code&gt;refundCreate&lt;/code&gt;, got back a &lt;code&gt;Refund&lt;/code&gt; object, and then told the customer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your refund has been processed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Looks reasonable, right?&lt;/p&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;Shopify’s docs are explicit: the existence of a &lt;code&gt;Refund&lt;/code&gt; object does not guarantee the financial transaction completed. The actual outcome lives on the related &lt;code&gt;OrderTransaction&lt;/code&gt; objects, which can be &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, or &lt;code&gt;failure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So this pattern is broken:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;call &lt;code&gt;refundCreate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;see &lt;code&gt;refund.id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tell the customer it’s done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s not confirmation. That’s optimism with JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mutation is not the problem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RefundOrder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-order-123"&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="n"&gt;refundCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gid://shopify/Order/123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer requested partial refund"&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="n"&gt;refund&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="n"&gt;id&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="n"&gt;userErrors&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="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug happens after the mutation.&lt;/p&gt;

&lt;p&gt;You need a second step that checks the transaction state before customer-facing messaging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safer pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refundInShopify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;refund&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;shopify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refundCreate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userErrors&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userErrors&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;txns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;shopify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrderTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;refundTxn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;txns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refundTxn&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No refund transaction found&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="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refundTxn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failure&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;default&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;That one extra verification step changes the customer message from guesswork into something defensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Stripe: timeouts turn "retry" into a reliability bug
&lt;/h2&gt;

&lt;p&gt;Stripe is where this gets dangerous.&lt;/p&gt;

&lt;p&gt;Imagine this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;your agent sends &lt;code&gt;POST /v1/refunds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;network hiccup&lt;/li&gt;
&lt;li&gt;worker times out&lt;/li&gt;
&lt;li&gt;response never gets persisted&lt;/li&gt;
&lt;li&gt;model decides to "try again"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you have uncertainty.&lt;/p&gt;

&lt;p&gt;Did Stripe create the refund?&lt;br&gt;
Did it fail?&lt;br&gt;
Did it succeed and you just lost the response?&lt;/p&gt;

&lt;p&gt;This is exactly why Stripe idempotency keys exist.&lt;/p&gt;

&lt;p&gt;And too many agent workflows still treat idempotency as optional.&lt;/p&gt;

&lt;p&gt;It’s not optional.&lt;/p&gt;

&lt;p&gt;It’s the thing that lets you retry without destroying your chain of evidence.&lt;/p&gt;
&lt;h3&gt;
  
  
  Correct Stripe call
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/refunds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"sk_test_...:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: 8b5b9f2e-6f8d-4f3d-a6d8-2f0f4d7f9c21"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ch_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Stripe stores the first result for a given idempotency key and returns the same status code and body on retries, including &lt;code&gt;500&lt;/code&gt; errors. If you reuse the same key with different parameters, Stripe rejects it.&lt;/p&gt;

&lt;p&gt;That means your workflow needs to persist the key before the call, not after.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bad retry logic
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;badRefundRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chargeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refunds&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="na"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chargeId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// terrible: new request identity, no certainty&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refunds&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="na"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chargeId&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;h3&gt;
  
  
  Better retry logic
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createRefundWithRecovery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chargeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existingKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existingKey&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refundAttempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;chargeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;started&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refunds&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chargeId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refundAttempts&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;chargeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rawResponse&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;refund&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="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refundAttempts&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;chargeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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;That &lt;code&gt;unknown&lt;/code&gt; state matters.&lt;/p&gt;

&lt;p&gt;A lot of teams try to avoid it because it feels messy. But "unknown" is honest. Telling the model to guess is not.&lt;/p&gt;
&lt;h2&gt;
  
  
  3) Zendesk: async jobs and rate limits expose toy workflows fast
&lt;/h2&gt;

&lt;p&gt;Zendesk is where polished demos usually fall apart.&lt;/p&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rate limits are not a suggestion
&lt;/h3&gt;

&lt;p&gt;Zendesk Support and Help Center API limits vary by plan. Responses can include headers like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Rate-Limit: 700
X-Rate-Limit-Remaining: 699
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you hit the limit, you can get &lt;code&gt;429 Too Many Requests&lt;/code&gt; plus &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A model that keeps hammering the API because it wants to be helpful is not helping.&lt;/p&gt;

&lt;p&gt;It’s just an expensive loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some actions are jobs, not immediate completion
&lt;/h3&gt;

&lt;p&gt;Bulk ticket updates are a classic example.&lt;/p&gt;

&lt;p&gt;The workflow sends the update, gets an acknowledgment, and assumes the tickets changed.&lt;/p&gt;

&lt;p&gt;But Zendesk job statuses can sit in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;queued&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;working&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent refunds 40 orders and bulk-updates 40 tickets, you cannot assume the ticket side finished just because the first call returned &lt;code&gt;200&lt;/code&gt; or &lt;code&gt;202&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You need to poll the job URL and reconcile failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal polling example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForZendeskJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobStatusUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;zendesk&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="nx"&gt;jobStatusUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&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="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Zendesk job failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;working&lt;/span&gt;&lt;span class="dl"&gt;"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown Zendesk job state: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Zendesk job did not reach terminal state in time&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not glamorous engineering.&lt;/p&gt;

&lt;p&gt;It is, however, the difference between a support agent that sounds polished and one that actually behaves reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt fixes vs orchestration fixes
&lt;/h2&gt;

&lt;p&gt;This is the distinction I wish I had made earlier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it actually solves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Better prompts and tighter tool descriptions&lt;/td&gt;
&lt;td&gt;Reduces bad reasoning and malformed calls before execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured Outputs in GPT-4o or GPT-5&lt;/td&gt;
&lt;td&gt;Improves schema adherence and valid arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency keys in Stripe and Shopify patterns&lt;/td&gt;
&lt;td&gt;Prevents uncertainty and duplicate side effects during retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling Shopify transactions and Zendesk jobs&lt;/td&gt;
&lt;td&gt;Confirms real-world completion instead of assuming it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable state in LangGraph, n8n, or Make&lt;/td&gt;
&lt;td&gt;Lets you recover after timeouts, 429s, crashes, or partial completion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is why I think a lot of "LLM tool use reliability" discussions are framed too narrowly.&lt;/p&gt;

&lt;p&gt;Once you touch money, tickets, orders, or customer records, you’re not debugging a chatbot anymore.&lt;/p&gt;

&lt;p&gt;You’re doing distributed systems work.&lt;/p&gt;

&lt;p&gt;The LLM is just one component in the chain.&lt;/p&gt;

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

&lt;p&gt;If I had to compress this into one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never let the model communicate success from the first side-effecting response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s the cheat sheet.&lt;/p&gt;

&lt;p&gt;| Check | Shopify refund flow | Stripe refund flow |&lt;br&gt;
|----------|----------|&lt;br&gt;
| Does the initial object guarantee money movement? | No. A &lt;code&gt;Refund&lt;/code&gt; object alone is not proof the refund settled | Usually the refund response is the main record, but retries must preserve idempotency to keep certainty |&lt;br&gt;
| Do you need post-call verification? | Yes. Check associated &lt;code&gt;OrderTransaction&lt;/code&gt; status like &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, or &lt;code&gt;failure&lt;/code&gt; | Yes, especially after timeouts or network failures; verify using the same idempotent request history or follow-up retrieval |&lt;br&gt;
| What should retries rely on? | Stored workflow state and explicit verification logic | The same idempotency key with the same parameters |&lt;/p&gt;

&lt;p&gt;That last row is where a lot of agent workflows quietly fail.&lt;/p&gt;

&lt;p&gt;Not because Shopify or Stripe are flaky.&lt;/p&gt;

&lt;p&gt;Because the workflow was stateless during retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you need LangGraph, or are n8n / Make enough?
&lt;/h2&gt;

&lt;p&gt;My opinion: not every support automation needs a full agent runtime.&lt;/p&gt;

&lt;p&gt;If the flow is mostly deterministic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;look up order&lt;/li&gt;
&lt;li&gt;issue refund&lt;/li&gt;
&lt;li&gt;update ticket&lt;/li&gt;
&lt;li&gt;send reply&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then &lt;code&gt;n8n&lt;/code&gt;, &lt;code&gt;Make&lt;/code&gt;, or even &lt;code&gt;Zapier&lt;/code&gt; with explicit branches can be safer than an autonomous loop.&lt;/p&gt;

&lt;p&gt;You can hard-code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;wait steps&lt;/li&gt;
&lt;li&gt;rate limit handling&lt;/li&gt;
&lt;li&gt;persisted IDs&lt;/li&gt;
&lt;li&gt;human escalation branches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the workflow is more open-ended and needs resumability across long windows, partial failure recovery, or branching investigation, then &lt;code&gt;LangGraph&lt;/code&gt; starts making more sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tiny LangGraph skeleton
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MessagesState&lt;/span&gt;&lt;span class="p"&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;END&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mock_llm&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;MessagesState&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;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="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;ai&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello world&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessagesState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mock_llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mock_llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hi!&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 example is trivial.&lt;/p&gt;

&lt;p&gt;The important part is the mindset shift: state transitions, checkpoints, resumability, deterministic recovery.&lt;/p&gt;

&lt;p&gt;Not "maybe GPT-5 or Claude Opus 4.6 will be more careful next time."&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that finally stopped the lying
&lt;/h2&gt;

&lt;p&gt;What actually fixed this for me was boring.&lt;/p&gt;

&lt;p&gt;I split the workflow into phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;prepare the action with validated inputs&lt;/li&gt;
&lt;li&gt;execute with an idempotency key or equivalent request identity&lt;/li&gt;
&lt;li&gt;persist external IDs and raw responses immediately&lt;/li&gt;
&lt;li&gt;verify downstream state in Shopify, Stripe, or Zendesk&lt;/li&gt;
&lt;li&gt;communicate only from verified state&lt;/li&gt;
&lt;li&gt;escalate ambiguous or non-terminal cases to a human&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That did more for reliability than all the prompt tuning combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you’re running AI agents in production, cost pressure makes this worse
&lt;/h2&gt;

&lt;p&gt;There’s one more thing people don’t talk about enough: per-token pricing pushes teams toward bad reliability decisions.&lt;/p&gt;

&lt;p&gt;When every retry, poll, verification step, and recovery branch feels like metered spend, people start trimming the boring parts.&lt;/p&gt;

&lt;p&gt;They skip verification.&lt;br&gt;
They shorten retries.&lt;br&gt;
They avoid durable state.&lt;br&gt;
They let the model improvise because it looks cheaper in the moment.&lt;/p&gt;

&lt;p&gt;That is exactly backwards.&lt;/p&gt;

&lt;p&gt;Production-grade agent workflows need room for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;polling&lt;/li&gt;
&lt;li&gt;recovery steps&lt;/li&gt;
&lt;li&gt;reconciliation passes&lt;/li&gt;
&lt;li&gt;long-running automations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a big reason tools like Standard Compute are interesting to teams building support agents, n8n flows, Make scenarios, and custom automations. If you’re using the OpenAI-compatible API shape but want predictable flat-cost compute instead of per-token anxiety, it changes how aggressively you can design for reliability.&lt;/p&gt;

&lt;p&gt;You stop asking, "Can we afford another verification pass?"&lt;/p&gt;

&lt;p&gt;You start asking the better question:&lt;/p&gt;

&lt;p&gt;"What would make this workflow stop lying to customers?"&lt;/p&gt;

&lt;p&gt;That’s the right optimization target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;If your refund agent sounds smart but sometimes lies, don’t assume the fix is a better prompt.&lt;/p&gt;

&lt;p&gt;Check whether your workflow is doing any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;confirming success from an initial API response&lt;/li&gt;
&lt;li&gt;retrying without idempotency&lt;/li&gt;
&lt;li&gt;failing to persist request identity&lt;/li&gt;
&lt;li&gt;skipping async job polling&lt;/li&gt;
&lt;li&gt;sending customer-facing messages before reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s usually where the real bug lives.&lt;/p&gt;

&lt;p&gt;The painful lesson for me was simple:&lt;/p&gt;

&lt;p&gt;Side effects are not chat turns.&lt;/p&gt;

&lt;p&gt;They’re distributed transactions wearing a chatbot costume.&lt;/p&gt;

&lt;p&gt;And once you treat them that way, your agents get a lot less charming and a lot more trustworthy.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>api</category>
    </item>
    <item>
      <title>We rebuilt 47-node n8n flows until we admitted script-first AI workflows are just better</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sun, 20 Sep 2026 06:08:48 +0000</pubDate>
      <link>https://dev.to/lars_winstand/we-rebuilt-47-node-n8n-flows-until-we-admitted-script-first-ai-workflows-are-just-better-1b9g</link>
      <guid>https://dev.to/lars_winstand/we-rebuilt-47-node-n8n-flows-until-we-admitted-script-first-ai-workflows-are-just-better-1b9g</guid>
      <description>&lt;h1&gt;
  
  
  We rebuilt 47-node n8n flows until we admitted script-first AI workflows are just better
&lt;/h1&gt;

&lt;p&gt;At 2:07 a.m., an n8n run failed because GPT-5.4 returned one ugly JSON blob that didn’t match what the next node expected.&lt;/p&gt;

&lt;p&gt;Not a big outage. Not a total failure.&lt;/p&gt;

&lt;p&gt;Just one malformed field buried inside a 47-node flow with branches, retries, fallback prompts, webhook handling, and a couple of emergency fixes living in an n8n Code node.&lt;/p&gt;

&lt;p&gt;So we did the usual thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opened execution history&lt;/li&gt;
&lt;li&gt;clicked through branches one by one&lt;/li&gt;
&lt;li&gt;checked whether the parser broke&lt;/li&gt;
&lt;li&gt;checked whether retry logic fired&lt;/li&gt;
&lt;li&gt;checked whether Claude Opus 4.6 behaved differently on the previous run&lt;/li&gt;
&lt;li&gt;tried to remember why one branch existed at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An hour later, we were still debugging a flow that was supposed to save time.&lt;/p&gt;

&lt;p&gt;That was the moment we stopped pretending visual AI workflows scale cleanly.&lt;/p&gt;

&lt;p&gt;My opinion now is pretty simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use n8n, Make, and Zapier for orchestration. Put the AI decision layer in code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your workflow has real branching, retries, schema validation, provider switching, and tests, script-first wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't n8n
&lt;/h2&gt;

&lt;p&gt;I like n8n.&lt;/p&gt;

&lt;p&gt;It's great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhooks&lt;/li&gt;
&lt;li&gt;cron jobs&lt;/li&gt;
&lt;li&gt;Slack notifications&lt;/li&gt;
&lt;li&gt;database writes&lt;/li&gt;
&lt;li&gt;approvals&lt;/li&gt;
&lt;li&gt;app-to-app glue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same story with Make and Zapier. They are useful because they remove a lot of boring integration work.&lt;/p&gt;

&lt;p&gt;The problem starts when your workflow tool becomes your application runtime.&lt;/p&gt;

&lt;p&gt;That works fine for deterministic automations.&lt;/p&gt;

&lt;p&gt;It gets ugly fast when the core logic depends on LLMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI workflows break in ways normal automations don't
&lt;/h2&gt;

&lt;p&gt;A normal automation is usually predictable enough that a visual graph stays readable.&lt;/p&gt;

&lt;p&gt;A lead comes in. You enrich it. You write to a CRM. You send a message.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;AI automations are different.&lt;/p&gt;

&lt;p&gt;Now you need to handle things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-5.4 returning valid JSON 95 times and weird prose on the 96th&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 extracting the right answer but missing your exact schema&lt;/li&gt;
&lt;li&gt;Grok 4.20 being good enough for one classification step but not another&lt;/li&gt;
&lt;li&gt;retries for rate limits, but not retries for bad outputs&lt;/li&gt;
&lt;li&gt;fallback prompts only after validation fails&lt;/li&gt;
&lt;li&gt;parsing rules shared across multiple automations&lt;/li&gt;
&lt;li&gt;provider swaps without rewriting the whole flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; model all of that with nodes and branches.&lt;/p&gt;

&lt;p&gt;We did.&lt;/p&gt;

&lt;p&gt;That's also how you end up with a workflow nobody wants to touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden tax is confidence
&lt;/h2&gt;

&lt;p&gt;The biggest problem wasn't breakage.&lt;/p&gt;

&lt;p&gt;It was confidence.&lt;/p&gt;

&lt;p&gt;Once your business logic lives across dozens of workflow nodes, every change feels dangerous.&lt;/p&gt;

&lt;p&gt;Rename one field? Maybe three branches break.&lt;/p&gt;

&lt;p&gt;Swap one model? Maybe parsing changes in five places.&lt;/p&gt;

&lt;p&gt;Add one retry path? Maybe a fallback branch now loops in a way nobody expected.&lt;/p&gt;

&lt;p&gt;This is where Make routers and Zapier Paths hit the same wall too. They look manageable at first. Then AI edge cases pile up and the whole thing turns into a visual choose-your-own-adventure with no real test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we tried before moving logic into code
&lt;/h2&gt;

&lt;p&gt;We tried to be disciplined inside the workflow builder.&lt;/p&gt;

&lt;p&gt;We:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cleaned up node names&lt;/li&gt;
&lt;li&gt;added comments&lt;/li&gt;
&lt;li&gt;split branches more clearly&lt;/li&gt;
&lt;li&gt;pushed parsing into an n8n Code node&lt;/li&gt;
&lt;li&gt;tightened prompts&lt;/li&gt;
&lt;li&gt;added validation steps&lt;/li&gt;
&lt;li&gt;added retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That helped for about five minutes.&lt;/p&gt;

&lt;p&gt;The real problem was architectural.&lt;/p&gt;

&lt;p&gt;We were using orchestration software as an app runtime.&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;versioning was bad&lt;/li&gt;
&lt;li&gt;reviewing changes was bad&lt;/li&gt;
&lt;li&gt;reusing logic was bad&lt;/li&gt;
&lt;li&gt;testing was mostly "run it and see"&lt;/li&gt;
&lt;li&gt;rollback was clumsy&lt;/li&gt;
&lt;li&gt;provider comparisons were annoying&lt;/li&gt;
&lt;li&gt;debugging meant clicking through execution trails instead of reading logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The breaking point came when we wanted to swap providers without rewriting the automation.&lt;/p&gt;

&lt;p&gt;That should be a config change.&lt;/p&gt;

&lt;p&gt;Inside a node maze, it turns into surgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should stay in n8n vs what should move to code
&lt;/h2&gt;

&lt;p&gt;Here's the split I wish we'd used earlier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keep in n8n / Make / Zapier&lt;/th&gt;
&lt;th&gt;Move to code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Triggers&lt;/td&gt;
&lt;td&gt;Prompt construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook entry points&lt;/td&gt;
&lt;td&gt;JSON schema validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedules&lt;/td&gt;
&lt;td&gt;Retry policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human approvals&lt;/td&gt;
&lt;td&gt;Fallback logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App-to-app handoffs&lt;/td&gt;
&lt;td&gt;Provider abstraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack / email / CRM actions&lt;/td&gt;
&lt;td&gt;Shared parsing rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database writes&lt;/td&gt;
&lt;td&gt;Testable business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow tools should orchestrate&lt;/li&gt;
&lt;li&gt;code should decide&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What script-first actually looks like
&lt;/h2&gt;

&lt;p&gt;You don't need to throw away n8n.&lt;/p&gt;

&lt;p&gt;Just stop asking it to own the hardest part.&lt;/p&gt;

&lt;p&gt;A much better pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;n8n receives the trigger&lt;/li&gt;
&lt;li&gt;n8n sends data to a script or internal service&lt;/li&gt;
&lt;li&gt;the service handles prompts, validation, retries, and model routing&lt;/li&gt;
&lt;li&gt;the service returns a stable payload&lt;/li&gt;
&lt;li&gt;n8n handles downstream actions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That service can be tiny. It doesn't need to be a giant platform.&lt;/p&gt;

&lt;p&gt;It just needs to put the AI logic somewhere that Git, tests, logs, and refactors actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example
&lt;/h2&gt;

&lt;p&gt;Let's say you have an n8n workflow that triages inbound support tickets.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fragile version
&lt;/h3&gt;

&lt;p&gt;Inside n8n:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call GPT-5.4&lt;/li&gt;
&lt;li&gt;parse JSON&lt;/li&gt;
&lt;li&gt;branch on category&lt;/li&gt;
&lt;li&gt;retry on failure&lt;/li&gt;
&lt;li&gt;call Claude Opus 4.6 if parsing fails&lt;/li&gt;
&lt;li&gt;reformat output&lt;/li&gt;
&lt;li&gt;branch again on urgency&lt;/li&gt;
&lt;li&gt;send to Slack or Zendesk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works until it doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  The better version
&lt;/h3&gt;

&lt;p&gt;n8n just calls your service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://internal-ai-service/triage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "ticket_id": "123",
    "subject": "Customer cannot log in",
    "body": "I reset my password twice and still get an error"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your service handles the messy part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&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;TicketSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;STANDARD_COMPUTE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.standardcompute.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;triageTicket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai/gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Classify support tickets and return strict JSON only.&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Subject: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nBody: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&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="s2"&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;response_format&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;json_object&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TicketSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&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;Now your workflow gets one stable response:&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;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urgency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&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 cannot log in after password reset attempts."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"needs_human"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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 is way easier to reason about than 8 branches and 3 parser nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is better for developers
&lt;/h2&gt;

&lt;p&gt;Once the AI layer is code-first, you get normal engineering tools back.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Real version control
&lt;/h3&gt;

&lt;p&gt;You can review prompt changes in Git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see exactly what changed in validation logic, retry policy, or provider selection.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tests
&lt;/h3&gt;

&lt;p&gt;You can write tests for the parts that matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triageTicket&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;classifies login issues as account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;result&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;triageTicket&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Locked out&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reset password twice, still can't log in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No, LLM tests won't be perfectly deterministic.&lt;/p&gt;

&lt;p&gt;They're still better than clicking "Execute Workflow" and hoping.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Shared logic
&lt;/h3&gt;

&lt;p&gt;One parser. One schema. One retry policy.&lt;/p&gt;

&lt;p&gt;Used everywhere.&lt;/p&gt;

&lt;p&gt;Not copied across five automations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Provider abstraction
&lt;/h3&gt;

&lt;p&gt;If GPT-5.4 is best for one task but Claude Opus 4.6 is better for another, that should be hidden behind one internal interface.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grok&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;gpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai/gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic/claude-opus-4.6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xai/grok-4.20&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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&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;That is a small change in code.&lt;/p&gt;

&lt;p&gt;Inside a giant workflow, it often becomes a full rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even more for 24/7 agents
&lt;/h2&gt;

&lt;p&gt;If a workflow runs twice a day, you can tolerate some mess.&lt;/p&gt;

&lt;p&gt;If an agent runs all day processing tickets, leads, documents, or support threads, every weak spot turns into operational drag.&lt;/p&gt;

&lt;p&gt;A flaky branch isn't a minor annoyance anymore.&lt;/p&gt;

&lt;p&gt;It's a permanent tax.&lt;/p&gt;

&lt;p&gt;This is also where cost and throughput start to matter.&lt;/p&gt;

&lt;p&gt;Teams running AI agents in n8n, Make, Zapier, OpenClaw, or custom workflows usually do not want to babysit token spend while background jobs keep firing.&lt;/p&gt;

&lt;p&gt;They want the automation to run.&lt;/p&gt;

&lt;p&gt;That is one reason Standard Compute is interesting in this setup.&lt;/p&gt;

&lt;p&gt;It gives you an OpenAI-compatible endpoint, so you can keep using the OpenAI SDK or any compatible HTTP client, while routing across GPT-5.4, Claude Opus 4.6, and Grok 4.20 behind the scenes.&lt;/p&gt;

&lt;p&gt;More importantly, it's flat-rate instead of per-token.&lt;/p&gt;

&lt;p&gt;That matters a lot for always-on automations, because architecture decisions stop being distorted by token anxiety.&lt;/p&gt;

&lt;p&gt;You can keep the script-first pattern, keep your orchestration layer the same, and avoid rebuilding workflows every time model pricing or provider quality shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple architecture that works
&lt;/h2&gt;

&lt;p&gt;If I were starting over, I'd use this split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n8n / Make / Zapier
  -&amp;gt; trigger, schedule, webhook, notifications
  -&amp;gt; call internal AI service

Internal AI service
  -&amp;gt; prompt construction
  -&amp;gt; model routing
  -&amp;gt; schema validation
  -&amp;gt; retries
  -&amp;gt; logging
  -&amp;gt; tests

Downstream systems
  -&amp;gt; Slack
  -&amp;gt; CRM
  -&amp;gt; database
  -&amp;gt; email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring architecture is underrated.&lt;/p&gt;

&lt;p&gt;This one is boring in the best possible way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actionable rule of thumb
&lt;/h2&gt;

&lt;p&gt;If your workflow has any of these, move the AI logic into code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more than one model provider&lt;/li&gt;
&lt;li&gt;strict JSON requirements&lt;/li&gt;
&lt;li&gt;retries based on failure type&lt;/li&gt;
&lt;li&gt;shared prompt logic&lt;/li&gt;
&lt;li&gt;reusable validation&lt;/li&gt;
&lt;li&gt;complex branching&lt;/li&gt;
&lt;li&gt;production consequences when it breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the workflow builder for orchestration.&lt;/p&gt;

&lt;p&gt;Keep the brain in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;The issue was never that n8n was bad.&lt;/p&gt;

&lt;p&gt;The issue was that we kept asking n8n to do a job better handled by code.&lt;/p&gt;

&lt;p&gt;Use n8n for orchestration.&lt;br&gt;
Use Make for orchestration.&lt;br&gt;
Use Zapier for orchestration.&lt;/p&gt;

&lt;p&gt;But if your AI workflow has real logic, real retries, real validation, and real provider switching, put that part in a script or service.&lt;/p&gt;

&lt;p&gt;We learned that after rebuilding broken flows more times than I'd like to admit.&lt;/p&gt;

&lt;p&gt;I wouldn't go back.&lt;/p&gt;

&lt;p&gt;If you're already running AI automations this way, I'm curious where you draw the line between workflow builder and code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>n8n</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>I tested VM vs Docker security for agent workers because “just use Docker” stopped feeling like an answer</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 19 Sep 2026 22:09:07 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-tested-vm-vs-docker-security-for-agent-workers-because-just-use-docker-stopped-feeling-like-an-4ia5</link>
      <guid>https://dev.to/lars_winstand/i-tested-vm-vs-docker-security-for-agent-workers-because-just-use-docker-stopped-feeling-like-an-4ia5</guid>
      <description>&lt;p&gt;A few months ago, I caught myself saying something I’d heard a hundred times before:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Just run it in Docker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the second it came out of my mouth, it sounded weak.&lt;/p&gt;

&lt;p&gt;Because we weren’t talking about a boring internal cron job.&lt;/p&gt;

&lt;p&gt;We were talking about agent workers that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open browser sessions&lt;/li&gt;
&lt;li&gt;run generated code&lt;/li&gt;
&lt;li&gt;call external APIs&lt;/li&gt;
&lt;li&gt;ingest weird files and HTML&lt;/li&gt;
&lt;li&gt;sometimes touch customer data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a very different threat model from “containerize the app and move on.”&lt;/p&gt;

&lt;p&gt;So I went back to the docs instead of repeating container folklore.&lt;/p&gt;

&lt;p&gt;The answer I landed on is not “Docker is unsafe” and it’s not “VMs everywhere.”&lt;/p&gt;

&lt;p&gt;It’s this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker shares the host Linux kernel. VMs and microVMs add a stronger isolation boundary. For trusted internal jobs, hardened rootless Docker is often enough. For browser agents, generated code, or multi-tenant data, I’d choose a VM or microVM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’re building workers for n8n, OpenClaw, Playwright, custom GPT-5 workflows, Claude-powered automations, or anything that looks like an AI agent runtime, the distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence in Docker’s docs that changed the whole conversation
&lt;/h2&gt;

&lt;p&gt;Docker is actually pretty clear if you read the security docs.&lt;/p&gt;

&lt;p&gt;Containers are not tiny VMs.&lt;/p&gt;

&lt;p&gt;They rely on the host Linux kernel for isolation.&lt;/p&gt;

&lt;p&gt;VMs don’t. They add a hardware virtualization boundary between guest and host.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but people still talk about Docker like it creates a hard wall by default. It doesn’t.&lt;/p&gt;

&lt;p&gt;Docker’s security model explicitly points you toward four areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kernel namespaces and cgroups&lt;/li&gt;
&lt;li&gt;the Docker daemon attack surface&lt;/li&gt;
&lt;li&gt;container configuration loopholes&lt;/li&gt;
&lt;li&gt;kernel hardening features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list tells you a lot.&lt;/p&gt;

&lt;p&gt;If your security argument is just “it’s in a container,” you’re skipping the part where Docker itself says configuration, daemon exposure, and kernel hardening all matter.&lt;/p&gt;

&lt;p&gt;That was my first real takeaway:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containerization is a starting point, not an isolation guarantee.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker is actually enough
&lt;/h2&gt;

&lt;p&gt;My opinion after re-reading all this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker is often fine for trusted, single-tenant internal workers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal automation triggered by your own team&lt;/li&gt;
&lt;li&gt;structured inputs&lt;/li&gt;
&lt;li&gt;calls to Notion, HubSpot, Salesforce, PostgreSQL&lt;/li&gt;
&lt;li&gt;no arbitrary code execution&lt;/li&gt;
&lt;li&gt;no hostile user content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not automatically require a VM.&lt;/p&gt;

&lt;p&gt;But there’s a huge difference between hardened Docker and lazy Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lazy version vs the version I’d actually trust
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The lazy version
&lt;/h3&gt;

&lt;p&gt;This is the version I see all the time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rootful Docker&lt;/li&gt;
&lt;li&gt;default seccomp&lt;/li&gt;
&lt;li&gt;broad Linux capabilities&lt;/li&gt;
&lt;li&gt;writable filesystem&lt;/li&gt;
&lt;li&gt;wide-open egress&lt;/li&gt;
&lt;li&gt;loose volume mounts&lt;/li&gt;
&lt;li&gt;default networking&lt;/li&gt;
&lt;li&gt;no AppArmor tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That setup is common.&lt;/p&gt;

&lt;p&gt;It’s also the exact setup behind a lot of false confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  The version I’d trust for lower-risk workers
&lt;/h3&gt;

&lt;p&gt;For a lower-risk internal worker, I’d want at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rootless Docker&lt;/li&gt;
&lt;li&gt;dropped Linux capabilities&lt;/li&gt;
&lt;li&gt;read-only filesystem where possible&lt;/li&gt;
&lt;li&gt;tight volume mounts&lt;/li&gt;
&lt;li&gt;outbound network restrictions&lt;/li&gt;
&lt;li&gt;AppArmor or another LSM policy&lt;/li&gt;
&lt;li&gt;a reviewed seccomp profile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That still does not give you VM-grade isolation.&lt;/p&gt;

&lt;p&gt;But it does materially reduce risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootless Docker is the first upgrade I’d make
&lt;/h2&gt;

&lt;p&gt;If I inherit a sketchy worker box running Playwright, Python job runners, or n8n sidecars, the first thing I’d check is whether Docker is running rootless.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because in rootless mode, both the Docker daemon and containers run as a non-root user.&lt;/p&gt;

&lt;p&gt;That changes the blast radius in a meaningful way.&lt;/p&gt;

&lt;p&gt;You can check the subordinate UID/GID mappings like this:&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="nb"&gt;grep&lt;/span&gt; ^&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;: /etc/subuid
&lt;span class="nb"&gt;grep&lt;/span&gt; ^&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;: /etc/subgid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker rootless mode expects subordinate ID ranges, typically at least 65536 IDs.&lt;/p&gt;

&lt;p&gt;Setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dockerd-rootless-setuptool.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s not a marketing checkbox. It’s a real change to how privilege is handled.&lt;/p&gt;

&lt;p&gt;If your current answer to agent isolation is “we use Docker,” but you’re still running rootful with broad defaults, I would fix that before arguing about anything fancier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seccomp helps, but it doesn’t turn a container into a VM
&lt;/h2&gt;

&lt;p&gt;This is another place where people overstate things.&lt;/p&gt;

&lt;p&gt;Docker’s default seccomp profile is useful.&lt;/p&gt;

&lt;p&gt;It blocks a set of syscalls and reduces attack surface.&lt;/p&gt;

&lt;p&gt;That’s good.&lt;/p&gt;

&lt;p&gt;But it is not a magic sandbox.&lt;/p&gt;

&lt;p&gt;You can run with a custom seccomp profile like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;seccomp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/seccomp/profile.json &lt;span class="se"&gt;\&lt;/span&gt;
  hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if the workload is sensitive enough, you probably should.&lt;/p&gt;

&lt;p&gt;Same story for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dropping capabilities&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;no-new-privileges&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;applying AppArmor&lt;/li&gt;
&lt;li&gt;locking down mounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cap-drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; no-new-privileges:true &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pids-limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256 &lt;span class="se"&gt;\&lt;/span&gt;
  my-worker:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s real hardening.&lt;/p&gt;

&lt;p&gt;But the core fact does not change:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hardened container is still a shared-kernel model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That matters a lot once your workers start doing agent-like things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment I stop trusting plain containers
&lt;/h2&gt;

&lt;p&gt;This is where my opinion gets less diplomatic.&lt;/p&gt;

&lt;p&gt;If a worker does any of the following, I think VM or microVM should be the default:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;runs browser agents with persistent sessions&lt;/li&gt;
&lt;li&gt;executes generated code from an LLM&lt;/li&gt;
&lt;li&gt;handles customer data across tenants&lt;/li&gt;
&lt;li&gt;pulls untrusted files, repos, PDFs, or HTML&lt;/li&gt;
&lt;li&gt;needs a security story that survives audit or review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because these workloads are messy.&lt;/p&gt;

&lt;p&gt;A browser agent is basically an automation engine pointed at untrusted content.&lt;/p&gt;

&lt;p&gt;A code-executing agent is literally running output you didn’t hand-write.&lt;/p&gt;

&lt;p&gt;A multi-tenant worker means one mistake can become a cross-customer incident.&lt;/p&gt;

&lt;p&gt;At that point, “we hardened Docker pretty well” starts sounding less like a strategy and more like a hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why gVisor, Kata Containers, and Firecracker exist
&lt;/h2&gt;

&lt;p&gt;The industry has already answered this problem several times.&lt;/p&gt;

&lt;p&gt;That’s why tools like these exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gVisor&lt;/li&gt;
&lt;li&gt;Kata Containers&lt;/li&gt;
&lt;li&gt;Firecracker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They all try to close the gap between container ergonomics and stronger isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  gVisor
&lt;/h3&gt;

&lt;p&gt;gVisor adds an extra defense layer by interposing a user-space kernel boundary.&lt;/p&gt;

&lt;p&gt;That’s useful when you still want container UX but don’t love the idea of untrusted code talking so directly to the host kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kata Containers
&lt;/h3&gt;

&lt;p&gt;Kata Containers uses lightweight VMs as a second isolation layer.&lt;/p&gt;

&lt;p&gt;That makes sense for teams that want something container-shaped operationally, but with stronger boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firecracker
&lt;/h3&gt;

&lt;p&gt;Firecracker is the one that made me take microVMs more seriously.&lt;/p&gt;

&lt;p&gt;It’s purpose-built for lightweight virtualization with minimal device surface and fast startup.&lt;/p&gt;

&lt;p&gt;And this is not lab-only infrastructure.&lt;/p&gt;

&lt;p&gt;Firecracker underpins AWS Lambda and AWS Fargate-style isolation patterns at enormous scale.&lt;/p&gt;

&lt;p&gt;That matters because it proves the model is practical, not theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  My practical map of the options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What I’d use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rootless Docker&lt;/td&gt;
&lt;td&gt;Trusted or moderately risky internal workers where shared-kernel isolation is acceptable and the team will actually harden the runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gVisor&lt;/td&gt;
&lt;td&gt;Untrusted code where container UX still matters and you want stronger syscall isolation than plain Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kata Containers / Firecracker microVMs&lt;/td&gt;
&lt;td&gt;Browser agents, code execution, or sensitive multi-tenant workflows where stronger isolation is worth the operational complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That’s the trade space as I see it.&lt;/p&gt;

&lt;p&gt;Not ideological. Just matching the boundary to the workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you pay for stronger isolation
&lt;/h2&gt;

&lt;p&gt;Nothing about stronger isolation is free.&lt;/p&gt;

&lt;p&gt;You usually pay in some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;startup latency&lt;/li&gt;
&lt;li&gt;throughput&lt;/li&gt;
&lt;li&gt;memory overhead&lt;/li&gt;
&lt;li&gt;compatibility quirks&lt;/li&gt;
&lt;li&gt;operational complexity&lt;/li&gt;
&lt;li&gt;debugging pain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why I also don’t agree with “always use VMs.”&lt;/p&gt;

&lt;p&gt;If the workload is low-risk and trusted, the VM tax can be unnecessary.&lt;/p&gt;

&lt;p&gt;But if the workload is externally influenced, chaotic, or tenant-sensitive, that tax starts looking cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete way to think about it
&lt;/h2&gt;

&lt;p&gt;Here’s a rough decision tree I’d use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use hardened rootless Docker when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the worker is single-tenant&lt;/li&gt;
&lt;li&gt;inputs are mostly trusted&lt;/li&gt;
&lt;li&gt;it does not execute arbitrary user-supplied code&lt;/li&gt;
&lt;li&gt;it does not process especially sensitive cross-customer data&lt;/li&gt;
&lt;li&gt;your team is willing to maintain hardening over time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use a VM or microVM when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the worker runs browser automation or browser agents&lt;/li&gt;
&lt;li&gt;the worker executes generated code&lt;/li&gt;
&lt;li&gt;the worker processes untrusted files or web content&lt;/li&gt;
&lt;li&gt;the worker serves multiple customers or tenants&lt;/li&gt;
&lt;li&gt;you need a cleaner security story for compliance or review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the practical answer I wish people gave more often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: a low-risk internal worker
&lt;/h2&gt;

&lt;p&gt;This is the kind of job I’m comfortable running in hardened Docker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;triggered by an internal webhook&lt;/li&gt;
&lt;li&gt;reads rows from PostgreSQL&lt;/li&gt;
&lt;li&gt;calls HubSpot and Slack&lt;/li&gt;
&lt;li&gt;writes results back&lt;/li&gt;
&lt;li&gt;no arbitrary code execution&lt;/li&gt;
&lt;li&gt;no browsing random websites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A run command might look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cap-drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; no-new-privileges:true &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pids-limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;internal_only &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /app/tmp:/tmp:rw &lt;span class="se"&gt;\&lt;/span&gt;
  my-internal-worker:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I still wouldn’t call that bulletproof.&lt;/p&gt;

&lt;p&gt;I would call it proportionate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: a worker I would move to a microVM immediately
&lt;/h2&gt;

&lt;p&gt;This one is different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts prompts from users&lt;/li&gt;
&lt;li&gt;launches Playwright or Chrome&lt;/li&gt;
&lt;li&gt;logs into third-party sites&lt;/li&gt;
&lt;li&gt;downloads files&lt;/li&gt;
&lt;li&gt;runs generated Python or JavaScript&lt;/li&gt;
&lt;li&gt;stores session state&lt;/li&gt;
&lt;li&gt;serves multiple customers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is microVM territory for me.&lt;/p&gt;

&lt;p&gt;Not because containers are useless.&lt;/p&gt;

&lt;p&gt;Because the workload is too exposed, too dynamic, and too difficult to reason about with a shared kernel boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  This gets more relevant as AI agents get cheaper to run
&lt;/h2&gt;

&lt;p&gt;There’s also an operational angle here.&lt;/p&gt;

&lt;p&gt;As more teams run agents continuously, cost pressure pushes them toward denser infrastructure and more automation.&lt;/p&gt;

&lt;p&gt;That’s exactly where people start making dangerous simplifications like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;put more workers on the same host&lt;/li&gt;
&lt;li&gt;let them run longer&lt;/li&gt;
&lt;li&gt;allow browser sessions to persist&lt;/li&gt;
&lt;li&gt;execute more generated code&lt;/li&gt;
&lt;li&gt;stop watching per-run cost so closely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I actually think predictable compute pricing makes this easier to reason about operationally.&lt;/p&gt;

&lt;p&gt;If you’re running lots of agent calls through an OpenAI-compatible endpoint like Standard Compute, you can stop obsessing over token billing and spend more time deciding where the real risk boundary should be.&lt;/p&gt;

&lt;p&gt;That’s the part people miss.&lt;/p&gt;

&lt;p&gt;Cheap or flat-rate model access does not remove the need for isolation.&lt;/p&gt;

&lt;p&gt;It increases the odds that you’ll run more autonomous workloads, more often, with less human supervision.&lt;/p&gt;

&lt;p&gt;Which makes the isolation decision more important, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me most
&lt;/h2&gt;

&lt;p&gt;I expected the answer to be mostly about technology.&lt;/p&gt;

&lt;p&gt;It wasn’t.&lt;/p&gt;

&lt;p&gt;It was about honesty.&lt;/p&gt;

&lt;p&gt;If someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We run it in Docker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That can mean two very different things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;we put a risky workload in a shared-kernel environment and did serious hardening&lt;/li&gt;
&lt;li&gt;we put a risky workload in a shared-kernel environment and hoped the defaults were enough&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are not the same sentence.&lt;/p&gt;

&lt;p&gt;For simple internal automations, Docker is often fine.&lt;/p&gt;

&lt;p&gt;For browser workers, code-executing agents, and customer-data workflows, I think VM or microVM is the safer default.&lt;/p&gt;

&lt;p&gt;Not because I’m anti-container.&lt;/p&gt;

&lt;p&gt;Because the docs are pretty clear once you stop treating “just use Docker” like a complete answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  My current rule of thumb
&lt;/h2&gt;

&lt;p&gt;If I trust the inputs and the blast radius is small, I’ll use hardened rootless Docker.&lt;/p&gt;

&lt;p&gt;If the worker browses the web, runs generated code, or touches multiple customers’ data, I want a VM or microVM boundary.&lt;/p&gt;

&lt;p&gt;That’s the line.&lt;/p&gt;

&lt;p&gt;And honestly, I think more agent infrastructure should start there.&lt;/p&gt;

&lt;p&gt;If you’re building AI workers on n8n, Make, Zapier, OpenClaw, or custom frameworks, this is also a good reminder to separate two decisions that people keep mixing together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how you call the model&lt;/li&gt;
&lt;li&gt;how you isolate the worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first problem, using an OpenAI-compatible endpoint with predictable pricing like Standard Compute can make agent workloads much easier to operate at scale.&lt;/p&gt;

&lt;p&gt;For the second problem, don’t let flat-rate inference lull you into weak runtime isolation.&lt;/p&gt;

&lt;p&gt;Different layer. Different risk. Same production system.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>We kept arguing about GPT vs Claude and missed the 2x long-context cost blowing up our agent runs</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 19 Sep 2026 14:09:28 +0000</pubDate>
      <link>https://dev.to/lars_winstand/we-kept-arguing-about-gpt-vs-claude-and-missed-the-2x-long-context-cost-blowing-up-our-agent-runs-21cm</link>
      <guid>https://dev.to/lars_winstand/we-kept-arguing-about-gpt-vs-claude-and-missed-the-2x-long-context-cost-blowing-up-our-agent-runs-21cm</guid>
      <description>&lt;p&gt;A thing finally clicked for me while debugging an agent trace:&lt;/p&gt;

&lt;p&gt;we were spending way too much time arguing about model choice, and not enough time looking at the giant pile of context getting replayed on every step.&lt;/p&gt;

&lt;p&gt;Not user intent.&lt;br&gt;
Not fresh instructions.&lt;br&gt;
Just baggage.&lt;/p&gt;

&lt;p&gt;Old tool output. Debug blobs. Previous assistant replies. Summaries of summaries. Random thread history nobody trimmed.&lt;/p&gt;

&lt;p&gt;That stuff is easy to ignore because it accumulates one harmless-looking decision at a time.&lt;/p&gt;

&lt;p&gt;But in a lot of agent workflows, especially tool-heavy ones, that is where the bill gets weird.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hidden cost wasn't the model switch
&lt;/h2&gt;

&lt;p&gt;The contrarian take:&lt;/p&gt;

&lt;p&gt;For a lot of agent pipelines, repeated input context matters more than the headline per-token rate.&lt;/p&gt;

&lt;p&gt;OpenAI's current pricing makes this painfully obvious. Long-context input is priced at 2x short-context input across the lineup, and long-context output is 1.5x higher too.&lt;/p&gt;

&lt;p&gt;One concrete example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gpt-5.6-terra&lt;/code&gt; short-context input: &lt;code&gt;$1.00 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpt-5.6-terra&lt;/code&gt; long-context input: &lt;code&gt;$2.00 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpt-5.6-terra&lt;/code&gt; short-context output: &lt;code&gt;$6.00 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpt-5.6-terra&lt;/code&gt; long-context output: &lt;code&gt;$9.00 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same model. Same app. Same team.&lt;/p&gt;

&lt;p&gt;The only difference is that one request is dragging around more history.&lt;/p&gt;

&lt;p&gt;That means a bloated thread can become a pricing decision all by itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this looks like in real agent code
&lt;/h2&gt;

&lt;p&gt;A lot of memory bugs don't look like bugs.&lt;/p&gt;

&lt;p&gt;They look like normal code:&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;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&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;google_genai:gemini-3.6-flash&lt;/span&gt;&lt;span class="sh"&gt;"&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;get_user_info&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InMemorySaver&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;thread_config&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;configurable&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;thread_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;1&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;Nothing scary there.&lt;/p&gt;

&lt;p&gt;But if that thread stays alive across multiple tool-heavy runs, the agent can keep pulling old state into new calls unless you explicitly trim or summarize it.&lt;/p&gt;

&lt;p&gt;And once tools are involved, each turn can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system instructions&lt;/li&gt;
&lt;li&gt;user messages&lt;/li&gt;
&lt;li&gt;assistant replies&lt;/li&gt;
&lt;li&gt;tool calls&lt;/li&gt;
&lt;li&gt;tool outputs&lt;/li&gt;
&lt;li&gt;intermediate state&lt;/li&gt;
&lt;li&gt;summaries&lt;/li&gt;
&lt;li&gt;more summaries later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple chatbot grows slowly.&lt;br&gt;
A workflow agent grows like a log file nobody rotates.&lt;/p&gt;
&lt;h2&gt;
  
  
  The biggest source of junk: tool output
&lt;/h2&gt;

&lt;p&gt;This is the one I see most often.&lt;/p&gt;

&lt;p&gt;A tool returns a huge JSON payload, and someone decides to pass the whole thing back into the next model call "just in case."&lt;/p&gt;

&lt;p&gt;Example:&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;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cus_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tickets"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;records&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"recent_orders"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;records&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"audit_log"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"crm_notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"very long string..."&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;Then the next step asks something tiny like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should we escalate this support ticket?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That decision probably needs 10 facts, not 10,000 tokens.&lt;/p&gt;

&lt;p&gt;A better pattern is:&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;raw_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_customer_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;store_raw_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;summary&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_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;raw_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;customer_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;open_ticket_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_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;tickets&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;recent_order_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_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;recent_orders&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;has_refund_last_30_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;has_recent_refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_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;priority_signals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extract_priority_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_result&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;tool&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the raw data outside the prompt.&lt;br&gt;
Pass only the facts needed for the next decision.&lt;/p&gt;
&lt;h2&gt;
  
  
  Observability data is not prompt data
&lt;/h2&gt;

&lt;p&gt;LangSmith traces are useful.&lt;br&gt;
OpenTelemetry spans are useful.&lt;br&gt;
Debug logs are useful.&lt;/p&gt;

&lt;p&gt;That does not mean they belong in your next model call.&lt;/p&gt;

&lt;p&gt;I've seen teams accidentally turn observability into recurring token spend by mirroring traces back into prompts so the model is "fully informed."&lt;/p&gt;

&lt;p&gt;That usually does two bad things at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;increases cost&lt;/li&gt;
&lt;li&gt;makes the model worse&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Too much stale context doesn't just cost more. It distracts the model.&lt;/p&gt;

&lt;p&gt;If your agent keeps seeing old tool results, outdated instructions, and irrelevant history, it starts anchoring on the wrong stuff.&lt;/p&gt;

&lt;p&gt;So this is not only a pricing problem.&lt;br&gt;
It's also an accuracy problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doesn't prompt caching solve this?
&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;It helps, but it does not make sloppy context free.&lt;/p&gt;

&lt;p&gt;OpenAI still charges for cached input and cache writes.&lt;br&gt;
For &lt;code&gt;gpt-5.6-terra&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cached input short-context: &lt;code&gt;$0.10 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cached input long-context: &lt;code&gt;$0.20 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cache write short-context: &lt;code&gt;$1.25 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cache write long-context: &lt;code&gt;$2.50 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic has the same basic story with different numbers. &lt;code&gt;Claude Opus 4.6&lt;/code&gt; pricing includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input: &lt;code&gt;$5 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;output: &lt;code&gt;$25 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;5-minute cache write: &lt;code&gt;$6.25 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;1-hour cache write: &lt;code&gt;$10 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cache hit/refresh: &lt;code&gt;$0.50 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google Gemini also treats caching as a paid feature. &lt;code&gt;Gemini 3.8 Flash&lt;/code&gt; in the paid tier is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input: &lt;code&gt;$1.50 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;output: &lt;code&gt;$3.75 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;context caching: &lt;code&gt;$0.15 / MTok&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cache storage fee starts in 2027&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, cache stable instructions and repeated reference material.&lt;/p&gt;

&lt;p&gt;But don't confuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;discounted repetition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no consequence&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The pricing differences that actually matter
&lt;/h2&gt;

&lt;p&gt;Model pricing matters.&lt;br&gt;
Model quality matters.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Claude Opus 4.6&lt;/code&gt; solves a planning task that &lt;code&gt;Claude Haiku 4.5&lt;/code&gt; keeps fumbling, paying more can be completely rational.&lt;/p&gt;

&lt;p&gt;If GPT-5 handles tool use or reasoning better for your workload, same story.&lt;/p&gt;

&lt;p&gt;But if your workflow is replaying irrelevant history on every step, even the cheaper model becomes an expensive habit.&lt;/p&gt;

&lt;p&gt;Here's the cleaner way to think about it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model / pricing view&lt;/th&gt;
&lt;th&gt;Key numbers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI short vs long context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.6-terra&lt;/code&gt;: short input &lt;code&gt;$1.00/MTok&lt;/code&gt;, long input &lt;code&gt;$2.00/MTok&lt;/code&gt;; short output &lt;code&gt;$6.00/MTok&lt;/code&gt;, long output &lt;code&gt;$9.00/MTok&lt;/code&gt;; cached input &lt;code&gt;$0.10/$0.20&lt;/code&gt;; cache writes &lt;code&gt;$1.25/$2.50&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;Input &lt;code&gt;$5/MTok&lt;/code&gt;, output &lt;code&gt;$25/MTok&lt;/code&gt;, 5-minute cache writes &lt;code&gt;$6.25/MTok&lt;/code&gt;, 1-hour cache writes &lt;code&gt;$10/MTok&lt;/code&gt;, cache hits/refreshes &lt;code&gt;$0.50/MTok&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;Input &lt;code&gt;$2/MTok&lt;/code&gt;, output &lt;code&gt;$10/MTok&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;Input &lt;code&gt;$1/MTok&lt;/code&gt;, output &lt;code&gt;$5/MTok&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;Input &lt;code&gt;$1.50/MTok&lt;/code&gt;, output &lt;code&gt;$3.75/MTok&lt;/code&gt;, context caching &lt;code&gt;$0.15/MTok&lt;/code&gt;, batch pricing is 50% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only compare vendors, you'll debate forever.&lt;/p&gt;

&lt;p&gt;If you inspect your traces, you'll ask the better question:&lt;/p&gt;

&lt;p&gt;why is this workflow paying to remember things it no longer needs?&lt;/p&gt;
&lt;h2&gt;
  
  
  5 practical fixes that reduce context without breaking the workflow
&lt;/h2&gt;

&lt;p&gt;You do not need a PhD in memory architecture for this.&lt;/p&gt;

&lt;p&gt;You need rules.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Stop replaying raw tool output by default
&lt;/h3&gt;

&lt;p&gt;If Salesforce, Jira, GitHub, Discord, or your internal API returns a giant object, do not feed the whole thing back into the model unless the next step actually needs it.&lt;/p&gt;

&lt;p&gt;Summarize first.&lt;br&gt;
Store the raw payload elsewhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Split working memory from audit memory
&lt;/h3&gt;

&lt;p&gt;Your prompt is not your log sink.&lt;/p&gt;

&lt;p&gt;Keep detailed traces in LangSmith, OpenTelemetry, Datadog, ClickHouse, BigQuery, or wherever you want.&lt;/p&gt;

&lt;p&gt;But only pass the model what it needs for the next decision.&lt;/p&gt;

&lt;p&gt;Working memory and audit memory should be different structures.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Summarize at step boundaries
&lt;/h3&gt;

&lt;p&gt;After every major tool call, compress the result into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;facts&lt;/li&gt;
&lt;li&gt;decisions made&lt;/li&gt;
&lt;li&gt;unresolved questions&lt;/li&gt;
&lt;li&gt;next action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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;compress_tool_result&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;dict&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="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;facts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extract_facts&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;decisions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extract_decisions&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;open_questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extract_open_questions&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;next_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;suggest_next_action&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the easiest ways to keep multi-step agent runs from ballooning.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Reset threads more often
&lt;/h3&gt;

&lt;p&gt;A lot of workflows should start fresh more often than they do.&lt;/p&gt;

&lt;p&gt;If an &lt;code&gt;n8n&lt;/code&gt; flow or &lt;code&gt;Make&lt;/code&gt; scenario is really a new job, give it a new thread.&lt;/p&gt;

&lt;p&gt;Don't let today's invoice lookup inherit yesterday's support escalation history just because both touched the same account.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keep stable instructions stable
&lt;/h3&gt;

&lt;p&gt;Large reusable instructions can benefit from caching.&lt;/p&gt;

&lt;p&gt;But volatile task state should stay separate.&lt;/p&gt;

&lt;p&gt;A small message list is often healthier than a giant rolling transcript:&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;messages&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;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;system&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant.&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize the latest support ticket and decide whether it needs escalation.&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;response&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;invoke&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple beats clever more often than agent builders like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick way to audit your own agent
&lt;/h2&gt;

&lt;p&gt;If you want a fast sanity check, log token counts and payload sizes per step.&lt;/p&gt;

&lt;p&gt;For example:&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;log_prompt_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_name&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;messages&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;total_chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&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="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;content&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;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="k"&gt;if&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;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;content&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;step&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;step_name&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_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approx_chars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_chars&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;Or if you're debugging a workflow locally, dump request bodies and inspect them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'.messages | length'&lt;/span&gt; request.json
jq &lt;span class="s1"&gt;'.messages[].role'&lt;/span&gt; request.json
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.messages[].content'&lt;/span&gt; request.json | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will usually find one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated system instructions&lt;/li&gt;
&lt;li&gt;giant tool payloads&lt;/li&gt;
&lt;li&gt;stale conversation turns&lt;/li&gt;
&lt;li&gt;duplicated summaries&lt;/li&gt;
&lt;li&gt;debug data that should never have been in the prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What if you actually need long context?
&lt;/h2&gt;

&lt;p&gt;Sometimes you do.&lt;/p&gt;

&lt;p&gt;Codebase-wide reasoning, contract analysis, compliance workflows, and deep research can genuinely need large windows.&lt;/p&gt;

&lt;p&gt;I'm not saying long context is fake.&lt;br&gt;
I'm saying default long context is lazy architecture.&lt;/p&gt;

&lt;p&gt;If you need it, use it deliberately.&lt;br&gt;
Route to it selectively.&lt;br&gt;
Budget for it.&lt;/p&gt;

&lt;p&gt;Treat expensive inference modes like an operational choice, not an accidental side effect of thread sprawl.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird part: oversized context can make agents dumber
&lt;/h2&gt;

&lt;p&gt;This was the part that surprised me most.&lt;/p&gt;

&lt;p&gt;I expected bloated context to cost more.&lt;br&gt;
I didn't expect it to so reliably make agents worse.&lt;/p&gt;

&lt;p&gt;But once you see it, it's obvious.&lt;/p&gt;

&lt;p&gt;The agent starts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anchoring on stale instructions&lt;/li&gt;
&lt;li&gt;revisiting old tool results&lt;/li&gt;
&lt;li&gt;answering the question from three turns ago&lt;/li&gt;
&lt;li&gt;slowing down because every call carries too much baggage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, model choice matters.&lt;/p&gt;

&lt;p&gt;But if your memory policy is a mess, benchmarking &lt;code&gt;GPT-5&lt;/code&gt; vs &lt;code&gt;Claude Opus 4.6&lt;/code&gt; vs &lt;code&gt;Grok 4.20&lt;/code&gt; is a little like comparing race cars while towing a dumpster.&lt;/p&gt;

&lt;h2&gt;
  
  
  My practical takeaway
&lt;/h2&gt;

&lt;p&gt;Before you switch models, inspect what your agent is dragging into every call.&lt;/p&gt;

&lt;p&gt;Cut stale turns.&lt;br&gt;
Summarize tool output.&lt;br&gt;
Separate traces from prompts.&lt;br&gt;
Reset threads more often.&lt;br&gt;
Use long context only when the task actually needs it.&lt;/p&gt;

&lt;p&gt;Most teams do not have a model problem first.&lt;/p&gt;

&lt;p&gt;They have a context hygiene problem with a pricing symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing: this is exactly why flat-rate AI compute is appealing
&lt;/h2&gt;

&lt;p&gt;If you're running agents all day in &lt;code&gt;n8n&lt;/code&gt;, &lt;code&gt;Make&lt;/code&gt;, &lt;code&gt;Zapier&lt;/code&gt;, OpenClaw, or custom workflows, per-token billing pushes teams into constant cost babysitting.&lt;/p&gt;

&lt;p&gt;And the worst part is that a lot of the cost comes from architecture mistakes like context sprawl, not from doing more useful work.&lt;/p&gt;

&lt;p&gt;That's a big reason products like Standard Compute are interesting: you get an OpenAI-compatible API, flat monthly pricing, and dynamic routing across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20 without every extra tool trace turning into another finance discussion.&lt;/p&gt;

&lt;p&gt;You still want clean prompts. Bad context can still hurt quality.&lt;/p&gt;

&lt;p&gt;But removing per-token anxiety makes it much easier to build agents that run continuously without treating every long thread like a budget incident.&lt;/p&gt;

&lt;p&gt;If you're building serious automations, that's a much better default than staring at token dashboards all week.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>OAuth redirect URI mismatch ate more time than my agent logic — 5 setup changes that finally fixed Google, Slack, and HubSpot</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 19 Sep 2026 06:09:15 +0000</pubDate>
      <link>https://dev.to/lars_winstand/oauth-redirect-uri-mismatch-ate-more-time-than-my-agent-logic-5-setup-changes-that-finally-fixed-41ia</link>
      <guid>https://dev.to/lars_winstand/oauth-redirect-uri-mismatch-ate-more-time-than-my-agent-logic-5-setup-changes-that-finally-fixed-41ia</guid>
      <description>&lt;p&gt;I lost more time to &lt;code&gt;oauth redirect uri mismatch&lt;/code&gt; than to the actual agent logic.&lt;/p&gt;

&lt;p&gt;Not the Gmail classification prompt.&lt;br&gt;
Not the Slack bot scopes.&lt;br&gt;
Not the HubSpot sync code.&lt;/p&gt;

&lt;p&gt;Just OAuth.&lt;/p&gt;

&lt;p&gt;The pattern was always the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;localhost worked&lt;/li&gt;
&lt;li&gt;staging mostly worked&lt;/li&gt;
&lt;li&gt;production failed with a vague callback error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building AI automations with Node.js, n8n, Slack apps, HubSpot apps, or Google integrations, this usually isn't random bad luck.&lt;/p&gt;

&lt;p&gt;It's architecture.&lt;/p&gt;

&lt;p&gt;The fix for us was boring but effective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one credential set per environment&lt;/li&gt;
&lt;li&gt;exact callback registration for each provider&lt;/li&gt;
&lt;li&gt;stable public base URLs&lt;/li&gt;
&lt;li&gt;logging the actual &lt;code&gt;redirect_uri&lt;/code&gt; being sent&lt;/li&gt;
&lt;li&gt;restarting workers after config changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once we did that, the "mystery OAuth bug" disappeared.&lt;/p&gt;
&lt;h2&gt;
  
  
  The real problem: your callback URL is telling two different stories
&lt;/h2&gt;

&lt;p&gt;Most teams assume OAuth failures mean the integration logic is broken.&lt;/p&gt;

&lt;p&gt;So they start debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt formatting&lt;/li&gt;
&lt;li&gt;token storage&lt;/li&gt;
&lt;li&gt;webhook handling&lt;/li&gt;
&lt;li&gt;SDK versions&lt;/li&gt;
&lt;li&gt;queue workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile the actual problem is usually this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected redirect URI:
https://app.example.com/auth/google/callback

Actual redirect URI sent:
https://api.example.com/auth/google/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com/auth/slack/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.example.com/auth/slack/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://staging.example.com/auth/hubspot/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when the provider expects HTTPS.&lt;/p&gt;

&lt;p&gt;OAuth is extremely literal.&lt;/p&gt;

&lt;p&gt;Your app can be conceptually correct and still fail because one URL string doesn't match exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What bit us across Google, Slack, and HubSpot
&lt;/h2&gt;

&lt;p&gt;Here's the short version.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What bites builders most often&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google OAuth 2.0 for Web Server Apps&lt;/td&gt;
&lt;td&gt;Redirect URI must match exactly. Teams also confuse localhost/native app patterns with real web-server OAuth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack OAuth v2&lt;/td&gt;
&lt;td&gt;If you send &lt;code&gt;redirect_uri&lt;/code&gt; in the authorize step, you must send the exact same &lt;code&gt;redirect_uri&lt;/code&gt; again during token exchange.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot OAuth 2.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redirect_uri&lt;/code&gt; is required, production redirects must use HTTPS, and app install often fails because the user is not a Super Admin.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these providers are especially weird.&lt;/p&gt;

&lt;p&gt;The weirdness comes from our stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frontend on one domain&lt;/li&gt;
&lt;li&gt;API on another&lt;/li&gt;
&lt;li&gt;reverse proxy in front&lt;/li&gt;
&lt;li&gt;background workers with stale env vars&lt;/li&gt;
&lt;li&gt;an automation tool generating callback URLs from the wrong base URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination creates bugs that feel random until you log the exact values.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Google: exact match means exact match
&lt;/h2&gt;

&lt;p&gt;Google was the easiest to misunderstand because local prototypes create bad habits.&lt;/p&gt;

&lt;p&gt;A lot of teams build something that works on localhost, then move it behind Vercel, Nginx, Cloud Run, or a custom Express server and assume the OAuth flow will survive the move.&lt;/p&gt;

&lt;p&gt;Sometimes it doesn't.&lt;/p&gt;

&lt;p&gt;For Google web-server OAuth, the registered redirect URI must match the one you send exactly.&lt;/p&gt;

&lt;p&gt;Not "same route, different host." Not "same callback, but HTTPS gets added later by the proxy."&lt;/p&gt;

&lt;p&gt;Exact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;Registered in Google Cloud Console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com/auth/google/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated by app code:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectUri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;PUBLIC_API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/google/callback`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;PUBLIC_API_BASE_URL&lt;/code&gt; is accidentally set to:&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="nv"&gt;PUBLIC_API_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then your flow is dead before your agent code runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple Node example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;googleapis&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;oauth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OAuth2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;process&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;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&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;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&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;GOOGLE_REDIRECT_URI&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;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oauth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAuthUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;access_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;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&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="s2"&gt;https://www.googleapis.com/auth/gmail.readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googleapis.com/auth/drive.readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;consent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Google auth URL:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Google redirect URI:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&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;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last log line matters more than people think.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Slack: the same redirect URI has to appear twice
&lt;/h2&gt;

&lt;p&gt;Slack was the one that felt unfair until I reread the docs carefully.&lt;/p&gt;

&lt;p&gt;The trap is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you send users to Slack's authorize URL&lt;/li&gt;
&lt;li&gt;you include &lt;code&gt;redirect_uri&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Slack sends back a code&lt;/li&gt;
&lt;li&gt;your backend exchanges the code for a token&lt;/li&gt;
&lt;li&gt;but your backend uses a different &lt;code&gt;redirect_uri&lt;/code&gt;, or omits it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: failure.&lt;/p&gt;

&lt;p&gt;If you include &lt;code&gt;redirect_uri&lt;/code&gt; during authorization, Slack expects the exact same value during token exchange.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorize step
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat:write,channels:history&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SLACK_REDIRECT_URI&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://slack.com/oauth/v2/authorize?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Token exchange step
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SLACK_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;SLACK_REDIRECT_URI&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;response&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;https://slack.com/api/oauth.v2.access&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;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="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those two &lt;code&gt;redirect_uri&lt;/code&gt; values differ, Slack is not the problem.&lt;/p&gt;

&lt;p&gt;Your app is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another Slack gotcha: scopes are split
&lt;/h3&gt;

&lt;p&gt;Slack also separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scope&lt;/code&gt; for bot scopes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_scope&lt;/code&gt; for user scopes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen teams mis-spec scopes, then blame OAuth in general.&lt;/p&gt;

&lt;p&gt;That bug looks a lot like redirect trouble at first because the install flow still fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) HubSpot: sometimes the URL is fine and the installer is wrong
&lt;/h2&gt;

&lt;p&gt;HubSpot gave us the most annoying fake OAuth bug.&lt;/p&gt;

&lt;p&gt;The flow itself is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send user to HubSpot authorize URL&lt;/li&gt;
&lt;li&gt;user approves scopes&lt;/li&gt;
&lt;li&gt;HubSpot redirects back with code&lt;/li&gt;
&lt;li&gt;backend exchanges code for token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basic example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;HUBSPOT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;HUBSPOT_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm.objects.contacts.read crm.objects.contacts.write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&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;randomUUID&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;installUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://app.hubspot.com/oauth/authorize?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&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="nx"&gt;installUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious rule is that &lt;code&gt;redirect_uri&lt;/code&gt; is required.&lt;/p&gt;

&lt;p&gt;The less obvious operational rule is that production redirects need HTTPS.&lt;/p&gt;

&lt;p&gt;But the one that wasted the most time for us was permissions.&lt;/p&gt;

&lt;p&gt;A user often needs Super Admin rights to install the app in a HubSpot account.&lt;/p&gt;

&lt;p&gt;So if your rollout fails, don't immediately assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your token exchange code is wrong&lt;/li&gt;
&lt;li&gt;your callback route is broken&lt;/li&gt;
&lt;li&gt;your state parameter failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the installer just isn't allowed to approve the app.&lt;/p&gt;

&lt;p&gt;That is not a code bug. It's an account-role problem wearing a code-bug costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 setup changes that finally fixed it
&lt;/h2&gt;

&lt;p&gt;This is the part I wish someone had handed me on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One credential set per environment
&lt;/h2&gt;

&lt;p&gt;Stop trying to make one OAuth app serve localhost, staging, and production cleanly.&lt;/p&gt;

&lt;p&gt;Use separate credentials.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google OAuth client for local&lt;/li&gt;
&lt;li&gt;Google OAuth client for staging&lt;/li&gt;
&lt;li&gt;Google OAuth client for prod&lt;/li&gt;
&lt;li&gt;separate Slack app config or redirect setup per environment&lt;/li&gt;
&lt;li&gt;separate HubSpot app settings where needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example env files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.local&lt;/span&gt;
&lt;span class="nv"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000/auth/google/callback
&lt;span class="nv"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000/auth/slack/callback
&lt;span class="nv"&gt;HUBSPOT_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000/auth/hubspot/callback
&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.staging&lt;/span&gt;
&lt;span class="nv"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://staging.example.com/auth/google/callback
&lt;span class="nv"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://staging.example.com/auth/slack/callback
&lt;span class="nv"&gt;HUBSPOT_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://staging.example.com/auth/hubspot/callback
&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.production&lt;/span&gt;
&lt;span class="nv"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://app.example.com/auth/google/callback
&lt;span class="nv"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://app.example.com/auth/slack/callback
&lt;span class="nv"&gt;HUBSPOT_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://app.example.com/auth/hubspot/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels tedious right up until it saves you two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Register every exact callback URL
&lt;/h2&gt;

&lt;p&gt;Not just the base domain.&lt;/p&gt;

&lt;p&gt;Not just one callback path you hope can cover everything.&lt;/p&gt;

&lt;p&gt;Register the exact callback URLs the provider will see.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/auth/google/callback
https://staging.example.com/auth/google/callback
https://app.example.com/auth/google/callback
https://app.example.com/auth/slack/callback
https://app.example.com/auth/hubspot/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app uses different subdomains for API and UI, decide which one owns the callback and stick to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use one stable public base URL
&lt;/h2&gt;

&lt;p&gt;A lot of OAuth bugs are really bad URL generation.&lt;/p&gt;

&lt;p&gt;The app builds redirect URIs from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request headers&lt;/li&gt;
&lt;li&gt;internal container hostnames&lt;/li&gt;
&lt;li&gt;editor URLs&lt;/li&gt;
&lt;li&gt;proxy-forwarded hosts&lt;/li&gt;
&lt;li&gt;stale env vars&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how you end up with callbacks pointing at the wrong domain.&lt;/p&gt;

&lt;p&gt;I prefer making the public base URL explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PUBLIC_APP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://app.example.com
&lt;span class="nv"&gt;PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build callbacks from one canonical value:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/google/callback`&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;SLACK_REDIRECT_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/slack/callback`&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;HUBSPOT_REDIRECT_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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;PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/hubspot/callback`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using n8n or similar tools, the public URL config matters a lot.&lt;/p&gt;

&lt;p&gt;Example:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VUE_APP_URL_BASE_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://n8n.example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the editor thinks it's running at one URL and the backend tells Google or Slack another story, you'll keep chasing ghosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Log the outbound authorize URL and inbound callback details
&lt;/h2&gt;

&lt;p&gt;This was the biggest practical win.&lt;/p&gt;

&lt;p&gt;Log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full authorize URL&lt;/li&gt;
&lt;li&gt;exact &lt;code&gt;redirect_uri&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;exact &lt;code&gt;state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;callback host/path/query&lt;/li&gt;
&lt;li&gt;token exchange payload fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not secrets, obviously.&lt;/p&gt;

&lt;p&gt;Just enough to compare what you intended with what actually happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Express middleware example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/google/start&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;redirectUri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;GOOGLE_REDIRECT_URI&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;state&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;randomUUID&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;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oauth2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAuthUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;access_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;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scope&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="s2"&gt;https://www.googleapis.com/auth/gmail.readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:start] redirect_uri=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirectUri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:start] state=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:start] authUrl=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&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;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/google/callback&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:callback] host=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:callback] originalUrl=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[google:callback] query=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you compare these logs across local, staging, and prod, the mismatch usually becomes obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Restart workers after config changes
&lt;/h2&gt;

&lt;p&gt;This sounds embarrassingly basic because it is.&lt;/p&gt;

&lt;p&gt;But long-running processes love stale config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queue consumers&lt;/li&gt;
&lt;li&gt;webhook handlers&lt;/li&gt;
&lt;li&gt;background job runners&lt;/li&gt;
&lt;li&gt;AI agent workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You update the dashboard or environment variables, but a worker still has the old redirect base cached in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical restart commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 restart all
&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;docker compose restart
&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;kubectl rollout restart deployment/my-api
kubectl rollout restart deployment/my-workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had workers generating old callback domains hours after the fix was supposedly deployed.&lt;/p&gt;

&lt;p&gt;That bug felt supernatural until we realized the processes had never reloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick debugging checklist
&lt;/h2&gt;

&lt;p&gt;When OAuth fails, this is the checklist I use now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redirect URI sanity check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$GOOGLE_REDIRECT_URI&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SLACK_REDIRECT_URI&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$HUBSPOT_REDIRECT_URI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Confirm what the app is actually sending
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"redirect_uri"&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inspect the callback route in logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; logs/app.log | &lt;span class="nb"&gt;grep &lt;/span&gt;callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify HTTPS in production
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com/auth/google/callback
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com/auth/slack/callback
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com/auth/hubspot/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check for proxy/header weirdness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trust proxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&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="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;forwardedProto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;forwardedHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-host&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="nf"&gt;next&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;If your app is behind a proxy and doesn't trust forwarded headers correctly, it may generate the wrong public URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  One question that can remove a lot of OAuth pain
&lt;/h2&gt;

&lt;p&gt;Do you actually need user OAuth?&lt;/p&gt;

&lt;p&gt;A lot of internal automations don't.&lt;/p&gt;

&lt;p&gt;If you're acting on shared infrastructure or project-owned resources, service accounts can be much simpler than user-consent flows.&lt;/p&gt;

&lt;p&gt;That won't replace Slack workspace installs or HubSpot app installs, obviously.&lt;/p&gt;

&lt;p&gt;But for some Google workflows, it's the difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handling consent screens&lt;/li&gt;
&lt;li&gt;storing refresh tokens&lt;/li&gt;
&lt;li&gt;managing callback URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and just using system-level credentials.&lt;/p&gt;

&lt;p&gt;Ask that question early.&lt;/p&gt;

&lt;p&gt;It can save a stupid amount of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more for AI agents and automations
&lt;/h2&gt;

&lt;p&gt;This problem gets worse when your stack includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n&lt;/li&gt;
&lt;li&gt;Make&lt;/li&gt;
&lt;li&gt;Zapier&lt;/li&gt;
&lt;li&gt;custom Node.js backends&lt;/li&gt;
&lt;li&gt;Slack bots&lt;/li&gt;
&lt;li&gt;CRM sync workers&lt;/li&gt;
&lt;li&gt;Gmail or Drive agents&lt;/li&gt;
&lt;li&gt;long-running background jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent systems are already distributed.&lt;/p&gt;

&lt;p&gt;Now add OAuth on top and every blurry architecture boundary gets exposed immediately.&lt;/p&gt;

&lt;p&gt;That's why these bugs feel so expensive.&lt;/p&gt;

&lt;p&gt;You're not just debugging one app.&lt;/p&gt;

&lt;p&gt;You're debugging identity across multiple services, multiple environments, and multiple callback assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson I took from this
&lt;/h2&gt;

&lt;p&gt;OAuth isn't hard because the protocol is mysterious.&lt;/p&gt;

&lt;p&gt;It's hard because it forces your architecture to stop lying.&lt;/p&gt;

&lt;p&gt;If local, staging, and production are fuzzy, OAuth will find the fuzziness.&lt;/p&gt;

&lt;p&gt;If your callback URL depends on whichever host answered the request, OAuth will punish that.&lt;/p&gt;

&lt;p&gt;If your HubSpot installer doesn't have Super Admin rights, no amount of token debugging will save you.&lt;/p&gt;

&lt;p&gt;The practical fix is boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate credentials by environment&lt;/li&gt;
&lt;li&gt;register exact callback URLs&lt;/li&gt;
&lt;li&gt;use a stable public base URL&lt;/li&gt;
&lt;li&gt;log what you actually send&lt;/li&gt;
&lt;li&gt;restart every process that might cache config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do that, and your agents can go back to failing for interesting reasons.&lt;/p&gt;




&lt;p&gt;One last thing: this is exactly the kind of problem that gets worse when every retry, every background worker run, and every agent loop has a per-token or per-call cost attached.&lt;/p&gt;

&lt;p&gt;If you're running lots of AI automations and agent workflows, predictable infrastructure matters just as much as correct OAuth setup. That's a big part of why Standard Compute exists: flat monthly AI compute for teams running automations all day, without watching token burn every time a workflow retries.&lt;/p&gt;

&lt;p&gt;If that's your situation, Standard Compute is worth a look: &lt;a href="https://standardcompute.com" rel="noopener noreferrer"&gt;https://standardcompute.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>node</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>The most expensive part of my agent stack wasn’t tokens — it was the invoice roulette</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Thu, 17 Sep 2026 22:09:54 +0000</pubDate>
      <link>https://dev.to/lars_winstand/the-most-expensive-part-of-my-agent-stack-wasnt-tokens-it-was-the-invoice-roulette-1ohk</link>
      <guid>https://dev.to/lars_winstand/the-most-expensive-part-of-my-agent-stack-wasnt-tokens-it-was-the-invoice-roulette-1ohk</guid>
      <description>&lt;p&gt;I hit a weird point with my agent stack a few months ago.&lt;/p&gt;

&lt;p&gt;Not when prompts were bad.&lt;br&gt;
Not when tools were failing.&lt;br&gt;
Not when n8n was half-broken.&lt;/p&gt;

&lt;p&gt;It happened after everything started working.&lt;/p&gt;

&lt;p&gt;The flows were stable. The AI Agent node in n8n was calling tools correctly. OpenClaw was keeping assistants alive across Slack and Discord. Background jobs were running. Users were happy.&lt;/p&gt;

&lt;p&gt;Then I opened the invoice.&lt;/p&gt;

&lt;p&gt;And I had that very specific engineering feeling: I understood every moving part individually, but I still could not predict the total cost.&lt;/p&gt;

&lt;p&gt;That was the moment I stopped thinking about AI cost as “price per token” and started thinking about billing volatility as an architecture problem.&lt;/p&gt;

&lt;p&gt;If you run agents 24/7, especially across n8n, Make, Zapier, OpenClaw, or custom workflows, this matters more than most pricing pages admit.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap: tokens are not the real unit of cost
&lt;/h2&gt;

&lt;p&gt;At prototype stage, token math feels clean.&lt;/p&gt;

&lt;p&gt;You estimate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;average prompt size&lt;/li&gt;
&lt;li&gt;average response size&lt;/li&gt;
&lt;li&gt;expected request volume&lt;/li&gt;
&lt;li&gt;maybe a little buffer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works for chat demos.&lt;/p&gt;

&lt;p&gt;It breaks for automations.&lt;/p&gt;

&lt;p&gt;An agent workflow is not one request. It is a chain reaction.&lt;/p&gt;

&lt;p&gt;A single inbound event can trigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one LLM call to interpret the task&lt;/li&gt;
&lt;li&gt;one or more tool calls&lt;/li&gt;
&lt;li&gt;another LLM call to summarize tool output&lt;/li&gt;
&lt;li&gt;retries after a timeout&lt;/li&gt;
&lt;li&gt;fallback to another provider after a rate limit&lt;/li&gt;
&lt;li&gt;an error workflow that replays part of the execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What looked like “1 event = 1 model call” turns into “1 event = 7 billable things plus side effects.”&lt;/p&gt;

&lt;p&gt;That is why production cost gets weird fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  A simple example from n8n
&lt;/h2&gt;

&lt;p&gt;Here is the kind of flow that looks cheap on a whiteboard and expensive in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Webhook receives a support ticket&lt;/li&gt;
&lt;li&gt;n8n AI Agent classifies urgency&lt;/li&gt;
&lt;li&gt;Agent calls CRM tool&lt;/li&gt;
&lt;li&gt;Agent calls search or knowledge base tool&lt;/li&gt;
&lt;li&gt;Agent drafts a reply&lt;/li&gt;
&lt;li&gt;Timeout happens on step 3&lt;/li&gt;
&lt;li&gt;Execution retries&lt;/li&gt;
&lt;li&gt;Draft step runs again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the pricing problem is not prompt length.&lt;/p&gt;

&lt;p&gt;It is workflow behavior.&lt;/p&gt;

&lt;p&gt;If you are tracking retries in n8n, you already know this pattern exists. Retry metadata like &lt;code&gt;execution.retryOf&lt;/code&gt; is a reminder that retries are normal in automation, not rare edge cases.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bill changes even when prompts don’t
&lt;/h2&gt;

&lt;p&gt;This is the part that made me stop trusting per-token forecasts.&lt;/p&gt;

&lt;p&gt;People describe usage-based pricing as transparent. In practice, for agent systems, it often isn’t.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because your bill is shaped by more than token volume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request bursts&lt;/li&gt;
&lt;li&gt;rate-limit backoffs&lt;/li&gt;
&lt;li&gt;queueing&lt;/li&gt;
&lt;li&gt;fallback chains&lt;/li&gt;
&lt;li&gt;cache hit rate&lt;/li&gt;
&lt;li&gt;background jobs&lt;/li&gt;
&lt;li&gt;sync vs batch routing&lt;/li&gt;
&lt;li&gt;tool fan-out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two months can have roughly the same user demand and still produce different costs.&lt;/p&gt;

&lt;p&gt;Not because prompts changed.&lt;br&gt;
Because execution shape changed.&lt;/p&gt;

&lt;p&gt;Maybe traffic got burstier.&lt;br&gt;
Maybe a nightly summarization job collided with interactive traffic.&lt;br&gt;
Maybe one provider throttled and your fallback path activated more often.&lt;br&gt;
Maybe your cache hit rate dropped because prompt prefixes drifted.&lt;/p&gt;

&lt;p&gt;That is invoice roulette.&lt;/p&gt;
&lt;h2&gt;
  
  
  Background jobs are where pricing models go to die
&lt;/h2&gt;

&lt;p&gt;The least honest part of many AI cost estimates is that they ignore background work.&lt;/p&gt;

&lt;p&gt;But real agent systems are full of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nightly classifiers&lt;/li&gt;
&lt;li&gt;memory refresh jobs&lt;/li&gt;
&lt;li&gt;ticket summarization&lt;/li&gt;
&lt;li&gt;thread cleanup&lt;/li&gt;
&lt;li&gt;webhook-triggered enrichments&lt;/li&gt;
&lt;li&gt;multi-channel assistants sitting idle-but-not-really-idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An OpenClaw assistant connected to Slack, Discord, Telegram, WhatsApp, and Teams may look quiet from the outside.&lt;/p&gt;

&lt;p&gt;It is still maintaining context, reacting to events, preserving sessions, and sometimes running scheduled tasks.&lt;/p&gt;

&lt;p&gt;That means cost keeps accumulating even when no human is actively chatting.&lt;/p&gt;
&lt;h2&gt;
  
  
  The vendor pricing features are useful — and also kind of a confession
&lt;/h2&gt;

&lt;p&gt;This was the part that surprised me most.&lt;/p&gt;

&lt;p&gt;Every major model vendor now has pricing features designed to reduce cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;batch APIs&lt;/li&gt;
&lt;li&gt;prompt caching&lt;/li&gt;
&lt;li&gt;context caching&lt;/li&gt;
&lt;li&gt;separate rate-limit pools&lt;/li&gt;
&lt;li&gt;grounding quotas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features are real and useful.&lt;/p&gt;

&lt;p&gt;They also quietly admit the same thing: raw synchronous per-request billing is a bad fit for a lot of production automation.&lt;/p&gt;
&lt;h2&gt;
  
  
  OpenAI Batch: good feature, loud signal
&lt;/h2&gt;

&lt;p&gt;OpenAI Batch is a solid option for offline work.&lt;/p&gt;

&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50% lower cost than synchronous API usage&lt;/li&gt;
&lt;li&gt;separate, higher-rate-limit capacity&lt;/li&gt;
&lt;li&gt;completion within 24 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bulk classification&lt;/li&gt;
&lt;li&gt;enrichment&lt;/li&gt;
&lt;li&gt;nightly summarization&lt;/li&gt;
&lt;li&gt;offline evaluation jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example shape:&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;# pseudo-workflow&lt;/span&gt;
&lt;span class="c"&gt;# 1. collect jobs during the day&lt;/span&gt;
&lt;span class="c"&gt;# 2. ship them to batch overnight&lt;/span&gt;
&lt;span class="c"&gt;# 3. read results later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing win is real.&lt;/p&gt;

&lt;p&gt;But the architectural implication matters more: now cost depends on execution mode, not just token count.&lt;/p&gt;

&lt;p&gt;You are no longer asking “how many tokens did I send?”&lt;br&gt;
You are asking “which jobs can tolerate delay, and did I route them correctly?”&lt;/p&gt;

&lt;p&gt;That is a workflow design problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Anthropic prompt caching: powerful, but easy to overestimate
&lt;/h2&gt;

&lt;p&gt;Anthropic caching looks fantastic on paper.&lt;/p&gt;

&lt;p&gt;Typical pricing structure for Claude tiers includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base input pricing&lt;/li&gt;
&lt;li&gt;separate cache write pricing&lt;/li&gt;
&lt;li&gt;much cheaper cache hit pricing&lt;/li&gt;
&lt;li&gt;output pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can be a huge win if your prompt prefixes are stable and requests arrive inside the cache window.&lt;/p&gt;

&lt;p&gt;Example:&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="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-opus-4-1&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a support triage assistant.&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="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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify this ticket and suggest next action.&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;cache_control&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is forecasting.&lt;/p&gt;

&lt;p&gt;Your savings now depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether prefixes actually stay stable&lt;/li&gt;
&lt;li&gt;whether requests land inside the cache lifetime&lt;/li&gt;
&lt;li&gt;whether your agent architecture reuses context consistently&lt;/li&gt;
&lt;li&gt;whether tool outputs keep mutating the prompt shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams often assume caching will save a lot, then discover their traffic pattern is too messy.&lt;/p&gt;

&lt;p&gt;For always-on agents, this is one of the easiest optimizations to model optimistically and realize pessimistically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Gemini Batch and caching: same story, more variables
&lt;/h2&gt;

&lt;p&gt;Google Gemini has the same pattern.&lt;/p&gt;

&lt;p&gt;There are real discounts for batch execution. There is context caching. There can also be extra costs around storage duration and grounding.&lt;/p&gt;

&lt;p&gt;Example batch call 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;from&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;genai&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;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;job&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;batches&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;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;src&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;contents&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="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;parts&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;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;Summarize this ticket in one sentence.&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;config&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;display_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;ticket-summary-batch&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, nothing is wrong with this.&lt;/p&gt;

&lt;p&gt;But now your cost model depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sync vs batch routing&lt;/li&gt;
&lt;li&gt;cache duration&lt;/li&gt;
&lt;li&gt;storage time&lt;/li&gt;
&lt;li&gt;grounding frequency&lt;/li&gt;
&lt;li&gt;request shape over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not simple token accounting anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which cost-saving feature is actually worth using?
&lt;/h2&gt;

&lt;p&gt;My opinion, after dealing with this in production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Batch&lt;/td&gt;
&lt;td&gt;Best for boring offline jobs like nightly classification, summarization, and enrichment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic prompt caching&lt;/td&gt;
&lt;td&gt;Best when prompts are highly stable and traffic repeatedly hits the same prefixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini Batch + caching&lt;/td&gt;
&lt;td&gt;Best when you can cleanly separate async work and actually manage caching/grounding behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My practical ranking:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Batch APIs are the easiest clear win&lt;/li&gt;
&lt;li&gt;Prompt caching is useful but easier to overestimate&lt;/li&gt;
&lt;li&gt;Complex multi-provider fallback systems create the worst forecasting problems&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you run an always-on assistant across Slack, Discord, Telegram, Teams, or WhatsApp, with memory, tools, webhooks, and scheduled jobs, the hardest part is not finding a low token price.&lt;/p&gt;

&lt;p&gt;It is explaining next month’s bill before next month happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem is invoice volatility
&lt;/h2&gt;

&lt;p&gt;This is the shift that changed how I think about AI infrastructure.&lt;/p&gt;

&lt;p&gt;The expensive part is not always the model.&lt;br&gt;
Sometimes it is the unpredictability.&lt;/p&gt;

&lt;p&gt;That matters because unpredictable spend changes engineering behavior.&lt;/p&gt;

&lt;p&gt;Teams start doing weird things when they cannot trust the bill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;throttling useful features too early&lt;/li&gt;
&lt;li&gt;avoiding background automation that would actually help users&lt;/li&gt;
&lt;li&gt;over-optimizing prompts instead of fixing workflow design&lt;/li&gt;
&lt;li&gt;delaying launches because finance wants tighter cost bounds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why predictable pricing becomes attractive long before raw per-token pricing becomes objectively expensive.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I do now instead of naive token forecasting
&lt;/h2&gt;

&lt;p&gt;I still care about model pricing.&lt;br&gt;
I just do forecasting differently.&lt;/p&gt;

&lt;p&gt;I model execution paths, not average prompts.&lt;/p&gt;

&lt;p&gt;That means I count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries per workflow&lt;/li&gt;
&lt;li&gt;average tool fan-out&lt;/li&gt;
&lt;li&gt;fallback frequency&lt;/li&gt;
&lt;li&gt;sync vs batch split&lt;/li&gt;
&lt;li&gt;cache hit assumptions&lt;/li&gt;
&lt;li&gt;background job frequency&lt;/li&gt;
&lt;li&gt;burst behavior under load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough spreadsheet is still useful, but only if it reflects system behavior.&lt;/p&gt;
&lt;h2&gt;
  
  
  A better way to think about cost
&lt;/h2&gt;

&lt;p&gt;Bad forecast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monthly_cost = avg_prompt_tokens * avg_response_tokens * requests * token_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better forecast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monthly_cost =
  interactive_requests * avg_interactive_execution_path
+ batch_requests * avg_batch_execution_path
+ background_jobs * avg_background_execution_path
+ retry_overhead
+ fallback_overhead
+ cache_miss_penalty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much closer to reality for agent systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability matters more when pricing gets harder to reason about
&lt;/h2&gt;

&lt;p&gt;If your workflows are complex, tracing becomes non-negotiable.&lt;/p&gt;

&lt;p&gt;For example, if you are debugging model behavior with LangSmith and OpenAI-compatible tooling:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LANGSMITH_TRACING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
export &lt;/span&gt;&lt;span class="nv"&gt;LANGSMITH_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-langsmith-api-key&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-openai-api-key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will not reduce the bill by itself.&lt;/p&gt;

&lt;p&gt;But it helps answer the question that always shows up too late:&lt;/p&gt;

&lt;p&gt;Why did this workflow call the model four times?&lt;/p&gt;

&lt;p&gt;Without tracing, cost debugging turns into archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  When usage-based pricing is still totally fine
&lt;/h2&gt;

&lt;p&gt;I do not think every team should abandon per-token billing.&lt;/p&gt;

&lt;p&gt;It is still a good fit when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Traffic is low-volume&lt;/li&gt;
&lt;li&gt;Workflows are simple&lt;/li&gt;
&lt;li&gt;Prompts are stable&lt;/li&gt;
&lt;li&gt;Retries are rare&lt;/li&gt;
&lt;li&gt;Background jobs are limited&lt;/li&gt;
&lt;li&gt;You can actually use batch or caching reliably&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In that world, usage-based pricing can absolutely be cheaper.&lt;/p&gt;

&lt;p&gt;But once agents run continuously, touch tools, operate across channels, and keep doing work while you sleep, predictability starts to matter more than benchmark token price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where flat-rate compute starts making more sense
&lt;/h2&gt;

&lt;p&gt;This is exactly why products like Standard Compute exist.&lt;/p&gt;

&lt;p&gt;If your stack already speaks the OpenAI API, swapping endpoints is much easier than rebuilding your workflows around five different pricing tricks.&lt;/p&gt;

&lt;p&gt;The appeal is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flat monthly pricing&lt;/li&gt;
&lt;li&gt;no per-token billing&lt;/li&gt;
&lt;li&gt;works with OpenAI-compatible SDKs and HTTP clients&lt;/li&gt;
&lt;li&gt;better fit for always-on agents and automations&lt;/li&gt;
&lt;li&gt;less time spent playing pricing Tetris across GPT, Claude, and Grok&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not remove the need for good architecture.&lt;/p&gt;

&lt;p&gt;You still need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tracing&lt;/li&gt;
&lt;li&gt;guardrails&lt;/li&gt;
&lt;li&gt;sane retry policies&lt;/li&gt;
&lt;li&gt;separation between real-time and batch work&lt;/li&gt;
&lt;li&gt;good tool design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it removes one category of chaos: surprise invoices caused by workflow behavior you did not model perfectly.&lt;/p&gt;

&lt;p&gt;For teams running n8n, Make, Zapier, OpenClaw, or custom agents, that tradeoff is often worth more than squeezing out the cheapest theoretical token path.&lt;/p&gt;

&lt;h2&gt;
  
  
  My takeaway
&lt;/h2&gt;

&lt;p&gt;The clean mental model is wrong.&lt;/p&gt;

&lt;p&gt;AI automation cost is not just model quality multiplied by token count.&lt;/p&gt;

&lt;p&gt;It is the sum of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;fallback chains&lt;/li&gt;
&lt;li&gt;cache windows&lt;/li&gt;
&lt;li&gt;batch queues&lt;/li&gt;
&lt;li&gt;grounding requests&lt;/li&gt;
&lt;li&gt;tool loops&lt;/li&gt;
&lt;li&gt;background jobs&lt;/li&gt;
&lt;li&gt;all the tiny workflow decisions that compound at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your biggest monthly question is no longer “which model is cheapest per token?” but “why can’t I predict this invoice at all?”&lt;/p&gt;

&lt;p&gt;That is not a finance problem.&lt;/p&gt;

&lt;p&gt;That is architecture.&lt;/p&gt;

&lt;p&gt;And once you see it that way, a lot of pricing decisions start looking very different.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>n8n</category>
      <category>openai</category>
    </item>
    <item>
      <title>I built a headless Mac mini AI server and by week two I was debugging sleep, plist files, and Redis</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Thu, 17 Sep 2026 14:10:25 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-built-a-headless-mac-mini-ai-server-and-by-week-two-i-was-debugging-sleep-plist-files-and-redis-2c9g</link>
      <guid>https://dev.to/lars_winstand/i-built-a-headless-mac-mini-ai-server-and-by-week-two-i-was-debugging-sleep-plist-files-and-redis-2c9g</guid>
      <description>&lt;p&gt;The moment I knew my cute little local AI box had turned into actual infrastructure was when a folder stopped moving files at 3:14 a.m.&lt;/p&gt;

&lt;p&gt;Nothing crashed.&lt;/p&gt;

&lt;p&gt;Disk was fine.&lt;/p&gt;

&lt;p&gt;Ollama was still up.&lt;/p&gt;

&lt;p&gt;The machine had just quietly stopped being useful.&lt;/p&gt;

&lt;p&gt;That was my headless Mac mini setup: a Mac mini on a shelf, no monitor, Ollama serving local models on &lt;code&gt;http://localhost:11434/v1&lt;/code&gt;, a few Python helpers, and n8n running background automations.&lt;/p&gt;

&lt;p&gt;For six days, it felt elegant.&lt;/p&gt;

&lt;p&gt;Then week two started.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real failure wasn't inference
&lt;/h2&gt;

&lt;p&gt;I expected model serving to be the hard part.&lt;/p&gt;

&lt;p&gt;Maybe Llama would be too slow.&lt;br&gt;
Maybe Qwen would eat RAM.&lt;br&gt;
Maybe Ollama would fall over under concurrent requests.&lt;/p&gt;

&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;The first reliability problem was sleep.&lt;/p&gt;

&lt;p&gt;A headless Mac mini loves to look alive while doing nothing useful.&lt;/p&gt;

&lt;p&gt;Your file watcher still exists.&lt;br&gt;
Your local API still responds sometimes.&lt;br&gt;
Your helper process is technically running.&lt;/p&gt;

&lt;p&gt;But if the machine slept, or the session changed in a way your setup didn't handle, your "automation server" became desktop theater.&lt;/p&gt;

&lt;p&gt;That was the first lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you don't explicitly manage power behavior, you're not running a server. You're running a desktop that occasionally pretends to be one.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The two commands that fixed the obvious nonsense
&lt;/h2&gt;

&lt;p&gt;If you're trying to keep a headless Mac mini alive for background jobs, you meet &lt;code&gt;caffeinate&lt;/code&gt; and &lt;code&gt;pmset&lt;/code&gt; fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; python3 watch.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prevents idle sleep while the command runs.&lt;/p&gt;

&lt;p&gt;For broader power settings:&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="nb"&gt;sudo &lt;/span&gt;pmset &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0
pmset &lt;span class="nt"&gt;-g&lt;/span&gt; assertions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does two useful things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;disables system sleep across power profiles&lt;/li&gt;
&lt;li&gt;shows which processes are currently asserting power management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or wrapping a long-running worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; /usr/local/bin/python3 /Users/you/ai-helper/watch.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One footgun worth calling out: &lt;code&gt;caffeinate -u&lt;/code&gt; defaults to a &lt;strong&gt;5 second timeout&lt;/strong&gt; if you don't pass &lt;code&gt;-t&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of detail that makes a test look fine and an overnight job fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;launchd&lt;/code&gt; matters more than another shell script
&lt;/h2&gt;

&lt;p&gt;A lot of people approach this with Linux habits.&lt;/p&gt;

&lt;p&gt;Start a process in the background.&lt;br&gt;
Add &lt;code&gt;&amp;amp;&lt;/code&gt;.&lt;br&gt;
Maybe use &lt;code&gt;nohup&lt;/code&gt;.&lt;br&gt;
Call it a service.&lt;/p&gt;

&lt;p&gt;On macOS, that gets messy fast.&lt;/p&gt;

&lt;p&gt;If the Mac mini is going to be headless and useful, &lt;code&gt;launchd&lt;/code&gt; is the real primitive.&lt;/p&gt;

&lt;p&gt;And Apple is pretty clear about one thing: jobs launched by &lt;code&gt;launchd&lt;/code&gt; should not daemonize themselves with the old fork-and-exit pattern.&lt;/p&gt;

&lt;p&gt;That means a lot of generic "run this as a service" tutorials are subtly wrong for macOS.&lt;/p&gt;

&lt;p&gt;Once I stopped fighting that, things got cleaner.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal &lt;code&gt;launchd&lt;/code&gt; watcher that actually behaves like a service
&lt;/h2&gt;

&lt;p&gt;For file-triggered helpers, a plist beats a Terminal tab every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.example.aihelper&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/local/bin/python3&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/ai-helper/watch.py&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;KeepAlive&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;WatchPaths&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/you/Inbox&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardOutPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;/tmp/aihelper.out.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardErrorPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;/tmp/aihelper.err.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three keys doing most of the work are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KeepAlive&lt;/code&gt;: restart the helper if it dies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WatchPaths&lt;/code&gt;: react to directory changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RunAtLoad&lt;/code&gt;: start immediately when loaded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Load it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl load ~/Library/LaunchAgents/com.example.aihelper.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on newer macOS versions, bootstrap it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl bootstrap gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; ~/Library/LaunchAgents/com.example.aihelper.plist
launchctl kickstart &lt;span class="nt"&gt;-k&lt;/span&gt; gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/com.example.aihelper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl print gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/com.example.aihelper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tail logs:&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="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/aihelper.out.log /tmp/aihelper.err.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the point where your Mac mini starts acting like a background worker instead of laptop cosplay.&lt;/p&gt;

&lt;p&gt;The downside is also immediate.&lt;/p&gt;

&lt;p&gt;You stop debugging only your Python code.&lt;br&gt;
Now you're debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plist syntax&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launchctl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;process environment differences&lt;/li&gt;
&lt;li&gt;restart behavior&lt;/li&gt;
&lt;li&gt;macOS logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a very different hobby.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ollama is easy right up until it becomes infrastructure
&lt;/h2&gt;

&lt;p&gt;I still think Ollama is the easiest on-ramp for local AI.&lt;/p&gt;

&lt;p&gt;Point your app at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:11434/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and use the OpenAI-compatible API.&lt;/p&gt;

&lt;p&gt;That means existing SDK code changes very little.&lt;/p&gt;

&lt;p&gt;Example with Python:&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;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/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;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&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;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&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;llama3.1&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="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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this file in 3 bullets.&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="nf"&gt;print&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;choices&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;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For experiments, this rules.&lt;/p&gt;

&lt;p&gt;You can swap models, test a summarizer, wire up an agent, and get the "wait, this actually works" feeling in under an hour.&lt;/p&gt;

&lt;p&gt;Then you realize Ollama is not a vibe.&lt;/p&gt;

&lt;p&gt;It's a server.&lt;/p&gt;

&lt;p&gt;And a server needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;startup behavior&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;restart behavior&lt;/li&gt;
&lt;li&gt;predictable uptime&lt;/li&gt;
&lt;li&gt;some answer to "what happens after a crash?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your AI helper is doing useful work while you're asleep, a Terminal tab is not a deployment strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real pain starts when one automation becomes five
&lt;/h2&gt;

&lt;p&gt;This is where local AI setups get oversold.&lt;/p&gt;

&lt;p&gt;A single user with a few lightweight jobs? Great.&lt;/p&gt;

&lt;p&gt;A Mac mini with &lt;code&gt;launchd&lt;/code&gt;, Ollama, and a couple scripts can be genuinely nice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;private&lt;/li&gt;
&lt;li&gt;fast on LAN&lt;/li&gt;
&lt;li&gt;fixed hardware cost&lt;/li&gt;
&lt;li&gt;no cloud bill anxiety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhooks&lt;/li&gt;
&lt;li&gt;file triggers&lt;/li&gt;
&lt;li&gt;scheduled jobs&lt;/li&gt;
&lt;li&gt;overlapping LLM calls&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;multiple users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and your tiny local setup starts acting like a small production system.&lt;/p&gt;

&lt;p&gt;That is where orchestration becomes the problem, not inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n gets real about concurrency faster than most people do
&lt;/h2&gt;

&lt;p&gt;n8n is a good example because the docs are pretty honest.&lt;/p&gt;

&lt;p&gt;In regular mode, production executions can pile up unless you cap them.&lt;/p&gt;

&lt;p&gt;The practical env var is:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;N8N_CONCURRENCY_PRODUCTION_LIMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Docker:&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;environment&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_CONCURRENCY_PRODUCTION_LIMIT=20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a tuning detail.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;It's the point where you admit overlapping work can absolutely make one box unresponsive.&lt;/p&gt;

&lt;p&gt;And once you need more than one busy process path, the architecture changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue mode is where your "local box" becomes a system
&lt;/h2&gt;

&lt;p&gt;n8n queue mode exists for a reason.&lt;/p&gt;

&lt;p&gt;It separates trigger handling from execution workers and uses Redis in the middle.&lt;/p&gt;

&lt;p&gt;That's the right move when jobs overlap heavily.&lt;/p&gt;

&lt;p&gt;It's also the point where your Mac mini is no longer just a local automation box.&lt;/p&gt;

&lt;p&gt;Now you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an n8n main instance&lt;/li&gt;
&lt;li&gt;one or more workers&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;shared encryption key management&lt;/li&gt;
&lt;li&gt;a database setup that should not be SQLite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a real operational jump.&lt;/p&gt;

&lt;p&gt;Here's the cleanest summary I can give:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What week two feels like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;launchd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native macOS fit for headless jobs, solid for watchers and helpers, but debugging moves into plist files and system behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n regular mode with concurrency cap&lt;/td&gt;
&lt;td&gt;Fine for a single instance and moderate load, but easy to outgrow once executions overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n queue mode&lt;/td&gt;
&lt;td&gt;Much better scaling story, but now you're operating Redis, workers, and actual workflow infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the local-first tax.&lt;/p&gt;

&lt;p&gt;Not API compatibility.&lt;br&gt;
Not whether Ollama can answer a prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration is where the maintenance cost shows up.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote admin is where the cute Mac mini story gets thin
&lt;/h2&gt;

&lt;p&gt;The internet makes headless Mac mini setups sound adorable.&lt;/p&gt;

&lt;p&gt;Tiny box.&lt;br&gt;
Silent.&lt;br&gt;
Efficient.&lt;br&gt;
Put it on a shelf and call it your AI server.&lt;/p&gt;

&lt;p&gt;Sure.&lt;/p&gt;

&lt;p&gt;But the minute you need reliable remote administration, you start touching way more of macOS than expected.&lt;/p&gt;

&lt;p&gt;SSH, power settings, permissions, launch agents vs launch daemons, login state, Full Disk Access edge cases.&lt;/p&gt;

&lt;p&gt;This is the part YouTube tutorials usually skip because it's less fun than benchmark screenshots.&lt;/p&gt;

&lt;p&gt;None of it is impossible.&lt;/p&gt;

&lt;p&gt;It's just not the clean little "local is simpler" story people like to tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  So was the Mac mini a bad idea?
&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;It was a good idea for exactly the amount of complexity I had on day one.&lt;/p&gt;

&lt;p&gt;That's the part I wish more people said out loud.&lt;/p&gt;

&lt;p&gt;Local hosting is not bad.&lt;/p&gt;

&lt;p&gt;A Mac mini can absolutely win on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;privacy&lt;/li&gt;
&lt;li&gt;LAN latency&lt;/li&gt;
&lt;li&gt;fixed hardware cost&lt;/li&gt;
&lt;li&gt;fast iteration for one person&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollama plus &lt;code&gt;launchd&lt;/code&gt; plus a couple helpers is a sane setup.&lt;/p&gt;

&lt;p&gt;The mistake is assuming a useful helper stays small.&lt;/p&gt;

&lt;p&gt;Useful helpers attract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more jobs&lt;/li&gt;
&lt;li&gt;more triggers&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;webhooks&lt;/li&gt;
&lt;li&gt;other users&lt;/li&gt;
&lt;li&gt;uptime expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's when local orchestration stops being fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule now
&lt;/h2&gt;

&lt;p&gt;If I'm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing prompts&lt;/li&gt;
&lt;li&gt;running a private summarizer&lt;/li&gt;
&lt;li&gt;building one-user automations&lt;/li&gt;
&lt;li&gt;experimenting with local models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still like the Mac mini.&lt;/p&gt;

&lt;p&gt;If I'm depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overlapping background executions&lt;/li&gt;
&lt;li&gt;reliable webhook handling&lt;/li&gt;
&lt;li&gt;worker behavior&lt;/li&gt;
&lt;li&gt;24/7 automations&lt;/li&gt;
&lt;li&gt;something I don't want to babysit at midnight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I stop pretending I'm "just running it locally."&lt;/p&gt;

&lt;p&gt;At that point I'm operating infrastructure, and I make decisions like it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most local AI posts skip
&lt;/h2&gt;

&lt;p&gt;The breaking point wasn't model performance.&lt;/p&gt;

&lt;p&gt;It wasn't even Apple being weird.&lt;/p&gt;

&lt;p&gt;It was realizing that &lt;strong&gt;local-first breaks down around orchestration long before it breaks down around inference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's why I think local model serving and production automation should be treated as separate decisions.&lt;/p&gt;

&lt;p&gt;You might absolutely want Ollama locally for privacy or speed.&lt;/p&gt;

&lt;p&gt;But if your agents, n8n workflows, or background automations are running constantly, the thing that hurts is usually not token generation.&lt;/p&gt;

&lt;p&gt;It's concurrency, retries, uptime, and cost predictability once usage stops being "a few tests" and turns into always-on work.&lt;/p&gt;

&lt;p&gt;That's also why flat-rate API infrastructure is more interesting than it used to be.&lt;/p&gt;

&lt;p&gt;If you're building agents or automations that hit LLMs all day, per-token pricing changes how you design everything. You start rationing calls, skipping useful steps, or watching cost dashboards instead of shipping.&lt;/p&gt;

&lt;p&gt;Standard Compute is interesting because it flips that tradeoff: OpenAI-compatible API, flat monthly pricing, and routing across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20 without the usual per-token anxiety.&lt;/p&gt;

&lt;p&gt;That's a very different fit from a shelf Mac mini, but it's solving the exact problem week two exposed for me: once the workflow matters, predictability matters more than the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical advice if you're building this right now
&lt;/h2&gt;

&lt;p&gt;If you're setting up a headless Mac mini for AI workflows, here's the short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Ollama for local experiments and private one-user tools.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;caffeinate&lt;/code&gt; and inspect &lt;code&gt;pmset&lt;/code&gt; before blaming your app.&lt;/li&gt;
&lt;li&gt;Move long-running jobs into &lt;code&gt;launchd&lt;/code&gt; early.&lt;/li&gt;
&lt;li&gt;Add logs from day one.&lt;/li&gt;
&lt;li&gt;Cap concurrency in n8n before the box teaches you why.&lt;/li&gt;
&lt;li&gt;Be honest about when "local" has become infrastructure.&lt;/li&gt;
&lt;li&gt;If your workflows run constantly, evaluate whether predictable API compute is actually simpler than self-hosting orchestration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole lesson.&lt;/p&gt;

&lt;p&gt;Week one was AI.&lt;/p&gt;

&lt;p&gt;Week two was operations.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>macos</category>
      <category>n8n</category>
      <category>devops</category>
    </item>
    <item>
      <title>I thought lead routing was a prompt problem until the AI assigned a California lead to nobody</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Thu, 17 Sep 2026 06:10:50 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-thought-lead-routing-was-a-prompt-problem-until-the-ai-assigned-a-california-lead-to-nobody-4kj1</link>
      <guid>https://dev.to/lars_winstand/i-thought-lead-routing-was-a-prompt-problem-until-the-ai-assigned-a-california-lead-to-nobody-4kj1</guid>
      <description>&lt;p&gt;I stopped trusting AI-first lead routing the day a valid inbound lead from California got assigned to… nobody.&lt;/p&gt;

&lt;p&gt;Not the wrong rep.&lt;/p&gt;

&lt;p&gt;Not the fallback SDR queue.&lt;/p&gt;

&lt;p&gt;Nobody.&lt;/p&gt;

&lt;p&gt;That’s the moment the whole architecture changed in my head.&lt;/p&gt;

&lt;p&gt;The prompt was fine. The model output looked reasonable. The bug was in the workflow logic around it.&lt;/p&gt;

&lt;p&gt;If you’re building CRM automation in n8n, Make, Zapier, OpenClaw, or a custom agent stack, this is the part that matters: LLMs are good at suggesting. They are bad candidates for being the final source of truth on ownership.&lt;/p&gt;

&lt;p&gt;The safe pattern is boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Let GPT-5 or Claude propose a segment or owner&lt;/li&gt;
&lt;li&gt;Validate with deterministic routing rules&lt;/li&gt;
&lt;li&gt;Send conflicts and low-confidence cases to a human queue&lt;/li&gt;
&lt;li&gt;Only then write the final owner to HubSpot or Salesforce&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds less exciting than “AI handles lead routing end-to-end.”&lt;/p&gt;

&lt;p&gt;It also works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real bug wasn’t the prompt
&lt;/h2&gt;

&lt;p&gt;The original workflow looked clean on a whiteboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the lead&lt;/li&gt;
&lt;li&gt;infer segment&lt;/li&gt;
&lt;li&gt;assign owner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern. Minimal. Very demo-friendly.&lt;/p&gt;

&lt;p&gt;But production systems don’t fail on whiteboards.&lt;/p&gt;

&lt;p&gt;Here’s what actually happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model suggested an owner based on company description and geography&lt;/li&gt;
&lt;li&gt;one branch checked territory&lt;/li&gt;
&lt;li&gt;another branch excluded house accounts&lt;/li&gt;
&lt;li&gt;another checked existing ownership rules&lt;/li&gt;
&lt;li&gt;two conditions overlapped&lt;/li&gt;
&lt;li&gt;one validation failed&lt;/li&gt;
&lt;li&gt;nothing handled the collision properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the record fell through the cracks.&lt;/p&gt;

&lt;p&gt;That’s not a prompt failure.&lt;/p&gt;

&lt;p&gt;That’s a control-plane failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLMs feel better at routing than they actually are
&lt;/h2&gt;

&lt;p&gt;Ask GPT-5, Claude Opus 4.6, or a solid open model to read a lead and suggest an owner.&lt;/p&gt;

&lt;p&gt;You’ll usually get something plausible.&lt;/p&gt;

&lt;p&gt;That’s the trap.&lt;/p&gt;

&lt;p&gt;Plausible is not governable.&lt;/p&gt;

&lt;p&gt;Real lead routing has rules like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;named accounts always stay with the account executive&lt;/li&gt;
&lt;li&gt;EMEA enterprise goes to one team except strategic partners&lt;/li&gt;
&lt;li&gt;California startup leads route one way unless they came from a partner form&lt;/li&gt;
&lt;li&gt;existing open opportunities override fresh inbound logic&lt;/li&gt;
&lt;li&gt;low-confidence enrichment should never trigger auto-assignment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An LLM can summarize those rules.&lt;/p&gt;

&lt;p&gt;It should not be the final authority on those rules.&lt;/p&gt;

&lt;p&gt;If it is, you’re going to spend Friday explaining weird ownership changes to RevOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture I trust
&lt;/h2&gt;

&lt;p&gt;This is the split I’d recommend for most teams:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Best owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normalize messy form input&lt;/td&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summarize enrichment&lt;/td&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infer probable segment from incomplete data&lt;/td&gt;
&lt;td&gt;LLM proposal only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce named account exclusions&lt;/td&gt;
&lt;td&gt;Deterministic rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce territory and geography&lt;/td&gt;
&lt;td&gt;Deterministic rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve conflicts&lt;/td&gt;
&lt;td&gt;Human review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write final owner to HubSpot or Salesforce&lt;/td&gt;
&lt;td&gt;Workflow after validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That split keeps the model in the fuzzy-data lane and keeps the workflow engine in the policy lane.&lt;/p&gt;

&lt;p&gt;If your routing is already based on clean fields like country, state, company size, or named account lists, you may not need an LLM at all.&lt;/p&gt;

&lt;p&gt;Seriously.&lt;/p&gt;

&lt;p&gt;A lot of teams add AI where a Switch node would have been enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n is a good example of where routing gets weird
&lt;/h2&gt;

&lt;p&gt;n8n makes the mechanics visible, which is useful.&lt;/p&gt;

&lt;p&gt;The Switch node has a few settings that matter a lot in lead routing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rules vs Expression mode&lt;/li&gt;
&lt;li&gt;Fallback Output behavior&lt;/li&gt;
&lt;li&gt;send to first matching output vs send to all matching outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is not cosmetic.&lt;/p&gt;

&lt;p&gt;If your routing rules overlap, these two settings create very different behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first matching output: quietly picks a winner&lt;/li&gt;
&lt;li&gt;all matching outputs: exposes that your rules collide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For lead routing, I want collisions exposed.&lt;/p&gt;

&lt;p&gt;If two territory rules match the same lead, that should go to review. I do not want the workflow silently pretending the first branch was obviously correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: bad routing shape
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudocode&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&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="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US Team&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;company_size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SMB&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="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;West SMB&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;named_account&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;account_executive&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;Looks harmless.&lt;/p&gt;

&lt;p&gt;But if multiple branches run and overwrite &lt;code&gt;owner&lt;/code&gt;, your audit trail becomes nonsense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better pattern: propose, validate, resolve
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiSuggestion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proposed_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;West SMB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proposed_segment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;California Startup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HQ in San Francisco, 42 employees, SaaS category&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;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;namedAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;named_account&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hasExistingOppOwner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open_opportunity_owner&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;matchesWestSMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company_size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SMB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;matchesPartnerException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partner_form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namedAccount&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="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_executive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;named_account_rule&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasExistingOppOwner&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="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open_opportunity_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;existing_opp_owner_rule&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aiSuggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&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="nf"&gt;sendToReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low_confidence_ai_suggestion&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchesWestSMB&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchesPartnerException&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="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;West SMB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;territory_rule_ca_smb&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendToReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_clear_owner&lt;/span&gt;&lt;span class="dl"&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 version is much less magical.&lt;/p&gt;

&lt;p&gt;It is also much easier to debug three weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  HubSpot already hints at the right answer
&lt;/h2&gt;

&lt;p&gt;HubSpot talks a lot now about AI-assisted workflow creation.&lt;/p&gt;

&lt;p&gt;Fine. Useful, even.&lt;/p&gt;

&lt;p&gt;But the part that actually keeps lead routing safe is still the old-school automation machinery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enrollment triggers&lt;/li&gt;
&lt;li&gt;re-enrollment controls&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;action history&lt;/li&gt;
&lt;li&gt;publishing controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the real story.&lt;/p&gt;

&lt;p&gt;The safe pattern inside HubSpot is not “AI decides owner.”&lt;/p&gt;

&lt;p&gt;It’s “AI helps annotate the lead, then governed workflow logic decides owner.”&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human fallback is not a cop-out
&lt;/h2&gt;

&lt;p&gt;It’s the correct design.&lt;/p&gt;

&lt;p&gt;n8n has documented a human fallback pattern for AI workflows, and it maps perfectly to lead routing.&lt;/p&gt;

&lt;p&gt;Use this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI proposes owner, segment, or missing structured fields&lt;/li&gt;
&lt;li&gt;deterministic rules validate territory, exclusions, and account ownership&lt;/li&gt;
&lt;li&gt;low-confidence or conflicting cases go to Slack&lt;/li&gt;
&lt;li&gt;human approves final owner&lt;/li&gt;
&lt;li&gt;workflow writes the owner back to HubSpot or Salesforce&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cases I would always send to review
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;enrichment confidence below threshold&lt;/li&gt;
&lt;li&gt;two territory rules match the same lead&lt;/li&gt;
&lt;li&gt;named account conflicts with geography routing&lt;/li&gt;
&lt;li&gt;existing opportunity owner conflicts with inbound owner&lt;/li&gt;
&lt;li&gt;the model inferred a critical field instead of reading it directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not anti-AI.&lt;/p&gt;

&lt;p&gt;That’s just adult supervision.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenClaw makes action easier, which raises the stakes
&lt;/h2&gt;

&lt;p&gt;Agent tooling is getting fast.&lt;/p&gt;

&lt;p&gt;OpenClaw, for example, is easy to stand up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://openclaw.ai/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; openclaw
openclaw onboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s great if you want to get an agent running quickly.&lt;/p&gt;

&lt;p&gt;But easy action is not the same thing as governed action.&lt;/p&gt;

&lt;p&gt;This is the pattern I keep seeing across agent stacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agents are getting better at doing things&lt;/li&gt;
&lt;li&gt;teams are still bad at explaining why those things happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CRM ownership, explanation is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the assignment auditable
&lt;/h2&gt;

&lt;p&gt;If you only take one thing from this post, make it this checklist.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Make AI output advisory
&lt;/h3&gt;

&lt;p&gt;Have GPT-5, Claude, Qwen, or Llama return a suggestion and confidence score.&lt;/p&gt;

&lt;p&gt;Not a final writeback.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep hard rules outside the prompt
&lt;/h3&gt;

&lt;p&gt;Named accounts, do-not-route lists, existing opportunity ownership, and territory exceptions should live in workflow logic or application code.&lt;/p&gt;

&lt;p&gt;Not buried in prompt prose.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Design for collisions on purpose
&lt;/h3&gt;

&lt;p&gt;If overlapping rules are possible, treat that as an exception path.&lt;/p&gt;

&lt;p&gt;Don’t hide it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep an exception queue
&lt;/h3&gt;

&lt;p&gt;Slack works.&lt;/p&gt;

&lt;p&gt;So does Jira, a HubSpot queue, or an internal review UI.&lt;/p&gt;

&lt;p&gt;Uncertain records need a place to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Store the reason for the final assignment
&lt;/h3&gt;

&lt;p&gt;Persist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI suggestion&lt;/li&gt;
&lt;li&gt;confidence score&lt;/li&gt;
&lt;li&gt;deterministic rule that won&lt;/li&gt;
&lt;li&gt;whether a human approved it&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone asks why a rep got a lead, “the agent decided” is not an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal implementation shape
&lt;/h2&gt;

&lt;p&gt;Here’s a simple pattern using an OpenAI-compatible client plus deterministic validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;OPENAI_BASE_URL&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;suggestRouting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Suggest a sales segment and owner for this lead.
Return JSON with: proposed_owner, proposed_segment, confidence, reason.
Lead: &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;lead&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateRouting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;named_account&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;final&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_executive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;named_account_rule&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open_opportunity_owner&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;final&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open_opportunity_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;existing_opp_owner_rule&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low_confidence&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company_size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SMB&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;final&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;West SMB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;territory_rule_ca_smb&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_matching_rule&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the shape.&lt;/p&gt;

&lt;p&gt;AI for interpretation.&lt;/p&gt;

&lt;p&gt;Code for policy.&lt;/p&gt;

&lt;p&gt;Humans for ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost problem shows up faster than people expect
&lt;/h2&gt;

&lt;p&gt;There’s another issue teams hit once they start doing this at volume: cost.&lt;/p&gt;

&lt;p&gt;Fallback-heavy workflows are expensive to iterate on when every test run is billed per token.&lt;/p&gt;

&lt;p&gt;Every one of these adds cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;branch testing&lt;/li&gt;
&lt;li&gt;confidence threshold tuning&lt;/li&gt;
&lt;li&gt;comparing GPT-5 vs Claude on segmentation&lt;/li&gt;
&lt;li&gt;sending borderline cases through multiple models&lt;/li&gt;
&lt;li&gt;testing human-review thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you’re building AI automations in n8n, Make, Zapier, OpenClaw, or custom OpenAI-compatible stacks, pricing affects architecture.&lt;/p&gt;

&lt;p&gt;If every experiment feels metered, teams test less.&lt;/p&gt;

&lt;p&gt;That usually means worse routing logic in production.&lt;/p&gt;

&lt;p&gt;This is exactly why flat-rate AI access is useful for workflow builders. With Standard Compute, you can keep the OpenAI-compatible API shape, route across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20, and iterate on real automation logic without treating every workflow run like a taximeter.&lt;/p&gt;

&lt;p&gt;That matters a lot when your workflow includes retries, fallbacks, and human-review loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  My opinionated take
&lt;/h2&gt;

&lt;p&gt;If an LLM is directly assigning owners in production without deterministic validation, you have not automated lead routing.&lt;/p&gt;

&lt;p&gt;You have automated future arguments.&lt;/p&gt;

&lt;p&gt;The easy part was getting Claude or GPT-5 to output a rep name.&lt;/p&gt;

&lt;p&gt;That demo works in five minutes.&lt;/p&gt;

&lt;p&gt;The hard part is building a system where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ownership rules are explicit&lt;/li&gt;
&lt;li&gt;confidence is checked&lt;/li&gt;
&lt;li&gt;collisions are visible&lt;/li&gt;
&lt;li&gt;exceptions stop for review&lt;/li&gt;
&lt;li&gt;every assignment is explainable later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the version that survives contact with actual sales teams.&lt;/p&gt;

&lt;p&gt;And if you’re doing it at scale, predictable AI cost matters almost as much as correct logic.&lt;/p&gt;

&lt;p&gt;Because a workflow you can’t afford to test properly is not production-ready either.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>n8n</category>
      <category>devops</category>
    </item>
    <item>
      <title>I cut my agent bill by shrinking the prompt, not the model</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Wed, 16 Sep 2026 22:09:24 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-cut-my-agent-bill-by-shrinking-the-prompt-not-the-model-e82</link>
      <guid>https://dev.to/lars_winstand/i-cut-my-agent-bill-by-shrinking-the-prompt-not-the-model-e82</guid>
      <description>&lt;p&gt;I kept trying to lower my agent costs by switching models.&lt;/p&gt;

&lt;p&gt;GPT-5 for one step. Claude Opus for planning. Gemini Flash for classification. Maybe Qwen for background work. Maybe Llama for cheap branches.&lt;/p&gt;

&lt;p&gt;That was not the main problem.&lt;/p&gt;

&lt;p&gt;The main problem was that my workflows were hauling too much junk into every request.&lt;/p&gt;

&lt;p&gt;An n8n agent. A Make scenario. A LangChain worker. A custom retry loop. None of them looked expensive alone. But together they kept rebilling the same bloated context on every step, retry, and branch.&lt;/p&gt;

&lt;p&gt;That was the leak.&lt;/p&gt;

&lt;p&gt;If you run LLM-powered automations, prompt compaction usually saves more money than another round of model shopping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expensive part usually isn't the model
&lt;/h2&gt;

&lt;p&gt;Model pricing is visible, so everyone fixates on it.&lt;/p&gt;

&lt;p&gt;Prompt bloat is worse because it hides in places that feel harmless:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full chat history&lt;/li&gt;
&lt;li&gt;giant tool schemas&lt;/li&gt;
&lt;li&gt;repeated document payloads&lt;/li&gt;
&lt;li&gt;stale memory from previous steps&lt;/li&gt;
&lt;li&gt;retries that resend almost the same prefix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an agent workflow, long context behaves like compound interest.&lt;/p&gt;

&lt;p&gt;One oversized prompt is annoying.&lt;br&gt;
A prompt that gets replayed across 20 steps, 3 retries, and 2 branches is a billing pattern.&lt;/p&gt;

&lt;p&gt;LangChain's short-term memory docs are pretty honest about this: long conversations make models slower, more expensive, and often worse because stale context distracts the model.&lt;/p&gt;

&lt;p&gt;That matches what I see in real workflows. Long context is not automatically intelligence. A lot of the time it's just clutter with a premium price tag.&lt;/p&gt;
&lt;h2&gt;
  
  
  What providers actually bill as context
&lt;/h2&gt;

&lt;p&gt;A lot of people think they're paying for "the user message."&lt;/p&gt;

&lt;p&gt;They're not.&lt;/p&gt;

&lt;p&gt;Providers count the full rendered request context. That usually includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system or developer messages&lt;/li&gt;
&lt;li&gt;tool definitions&lt;/li&gt;
&lt;li&gt;JSON schemas&lt;/li&gt;
&lt;li&gt;memory/history&lt;/li&gt;
&lt;li&gt;retrieved documents&lt;/li&gt;
&lt;li&gt;images or other multimodal payloads&lt;/li&gt;
&lt;li&gt;the actual user input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if your agent step only needs &lt;code&gt;search_tickets&lt;/code&gt; and &lt;code&gt;update_crm_record&lt;/code&gt;, but you're sending 12 tool schemas and a full transcript anyway, you're paying for dead weight.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 3 biggest prompt bloat problems I keep seeing
&lt;/h2&gt;

&lt;p&gt;Across n8n, Make, OpenClaw, LangChain, and custom workers, the same problems show up over and over.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Full chat transcripts on every step
&lt;/h3&gt;

&lt;p&gt;Most steps do not need the entire conversation.&lt;/p&gt;

&lt;p&gt;Usually they need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a compact state summary&lt;/li&gt;
&lt;li&gt;the latest user instruction&lt;/li&gt;
&lt;li&gt;maybe the last 1-3 turns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passing 40 turns of history into a tool-selection step is how you quietly light money on fire.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Huge tool schemas everywhere
&lt;/h3&gt;

&lt;p&gt;This one is common in agent frameworks.&lt;/p&gt;

&lt;p&gt;You register a big toolset once, then every request drags all of it along.&lt;/p&gt;

&lt;p&gt;If a step only needs two tools, send two tools.&lt;/p&gt;

&lt;p&gt;Bad:&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;"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="s2"&gt;"search_tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"update_crm_record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"create_invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"sync_calendar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"fetch_slack_thread"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"query_warehouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"generate_contract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"log_incident"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"create_jira_issue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"archive_conversation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"notify_webhook"&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;Better:&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;"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="s2"&gt;"search_tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"update_crm_record"&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;h3&gt;
  
  
  3. Re-sending big document payloads
&lt;/h3&gt;

&lt;p&gt;A lot of automations still paste giant docs into prompts instead of retrieving only the relevant chunks.&lt;/p&gt;

&lt;p&gt;If the model needs one policy paragraph, don't send the whole handbook.&lt;/p&gt;

&lt;p&gt;If it needs one support ticket, don't send the whole account history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt caching helps, but not as much as people hope
&lt;/h2&gt;

&lt;p&gt;Prompt caching is real.&lt;/p&gt;

&lt;p&gt;But caching rewards stability, not chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenAI
&lt;/h3&gt;

&lt;p&gt;OpenAI says cached-input discounts can reach up to 90%.&lt;/p&gt;

&lt;p&gt;That's great.&lt;/p&gt;

&lt;p&gt;The catch is that cache reuse depends on the full rendered prefix matching exactly. If you change something early in the prompt, mutate tool definitions, reorder context, or inject slightly different memory, you can miss the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic
&lt;/h3&gt;

&lt;p&gt;Anthropic prompt caching is useful too, but the default cache lifetime is 5 minutes.&lt;/p&gt;

&lt;p&gt;That works well for tight loops.&lt;/p&gt;

&lt;p&gt;It helps less when your automation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pauses between steps&lt;/li&gt;
&lt;li&gt;retries later&lt;/li&gt;
&lt;li&gt;branches asynchronously&lt;/li&gt;
&lt;li&gt;wakes up on delayed triggers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gemini
&lt;/h3&gt;

&lt;p&gt;Gemini implicit caching is enabled by default on newer models, but requests still need to cross token thresholds before it matters.&lt;/p&gt;

&lt;p&gt;So yes, caching is useful.&lt;/p&gt;

&lt;p&gt;But "maybe the cache saves me" is not a cost strategy.&lt;/p&gt;

&lt;p&gt;Compacting the prompt is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider caching reality, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What actually matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Prompt Caching&lt;/td&gt;
&lt;td&gt;Exact rendered prefix match is required; tools, schemas, developer messages, and history all affect reuse; cached-input discount can reach up to 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Prompt Caching&lt;/td&gt;
&lt;td&gt;Helpful for repeated prefixes, but default cache lifetime is 5 minutes; better for tight loops than delayed automations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini Implicit Caching&lt;/td&gt;
&lt;td&gt;Enabled by default on newer Gemini models, but only helps after minimum token thresholds are crossed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is simple:&lt;/p&gt;

&lt;p&gt;Caching is great when your workflow is repetitive and tightly controlled.&lt;/p&gt;

&lt;p&gt;Most real-world automations are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n already points you toward trimming memory
&lt;/h2&gt;

&lt;p&gt;This part matters for anyone building agents in n8n.&lt;/p&gt;

&lt;p&gt;n8n documents memory patterns pretty clearly, including memory backends and the Chat Memory Manager node. The docs explicitly point you toward inspecting and reducing memory before handing it to an Agent node.&lt;/p&gt;

&lt;p&gt;That's not an advanced trick.&lt;/p&gt;

&lt;p&gt;That's routine maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The n8n pattern I like
&lt;/h3&gt;

&lt;p&gt;If I'm building an n8n agent now, I try to do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep short-term memory separate from durable app data&lt;/li&gt;
&lt;li&gt;Summarize or trim history before the Agent node&lt;/li&gt;
&lt;li&gt;Pass only the tools needed for that step&lt;/li&gt;
&lt;li&gt;Retrieve only the document chunks relevant to the current action&lt;/li&gt;
&lt;li&gt;Save a compact state summary instead of the full transcript&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That usually improves both cost and output quality.&lt;/p&gt;

&lt;p&gt;Because agents are terrible roommates. Give them too much stuff and they stop finding what matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How memory bloat spreads in LangChain-style agents
&lt;/h2&gt;

&lt;p&gt;The architecture makes the problem obvious.&lt;/p&gt;

&lt;p&gt;Memory gets read before the next action, then updated after the action. If you don't manage it, every bad step becomes future baggage.&lt;/p&gt;

&lt;p&gt;Example:&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;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&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;google_genai:gemini-3.6-flash&lt;/span&gt;&lt;span class="sh"&gt;"&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;get_user_info&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InMemorySaver&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;That setup is convenient.&lt;/p&gt;

&lt;p&gt;It's also how one noisy tool call turns into repeated prompt cost for the rest of the workflow.&lt;/p&gt;

&lt;p&gt;A failed attempt gets written.&lt;br&gt;
Then a retry.&lt;br&gt;
Then another tool result.&lt;br&gt;
Then a side branch.&lt;br&gt;
Then a transcript chunk nobody needs anymore.&lt;/p&gt;

&lt;p&gt;Soon your "memory" is just a landfill the model has to search before answering a simple question.&lt;/p&gt;
&lt;h2&gt;
  
  
  A practical compaction pass
&lt;/h2&gt;

&lt;p&gt;Here's the kind of cleanup I do before touching model selection.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before: bloated request assembly
&lt;/h3&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;build_prompt&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;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docs&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;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;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&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;full_chat_history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;all_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&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;current_task&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;h3&gt;
  
  
  After: compact request assembly
&lt;/h3&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;build_prompt&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;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docs&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;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;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&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;summary&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;last_turns&lt;/span&gt;&lt;span class="p"&gt;[&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;tools&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;required_for_step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_k_chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&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;current_task&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;That change is boring.&lt;/p&gt;

&lt;p&gt;It also tends to save real money.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick ways to inspect prompt size
&lt;/h2&gt;

&lt;p&gt;If you're not measuring, you'll miss the problem.&lt;/p&gt;

&lt;p&gt;A simple first step is logging approximate payload size before each model call.&lt;/p&gt;
&lt;h3&gt;
  
  
  Python example
&lt;/h3&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;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;approx_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;len&lt;/span&gt;&lt;span class="p"&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;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&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;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docs&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="s"&gt;prompt_payload_chars=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;approx_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Node.js example
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;approxChars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="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;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&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="s2"&gt;`prompt_payload_chars=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;approxChars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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 isn't token-accurate, but it's enough to catch obvious growth.&lt;/p&gt;

&lt;p&gt;If one workflow step is shipping 10x more payload than the others, that's where I'd start.&lt;/p&gt;
&lt;h2&gt;
  
  
  A useful workflow audit checklist
&lt;/h2&gt;

&lt;p&gt;Before I debate GPT-5 vs Claude Opus vs Gemini Flash, I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this step really need the full conversation history?&lt;/li&gt;
&lt;li&gt;Can I replace transcript history with a rolling summary plus the last few turns?&lt;/li&gt;
&lt;li&gt;Are unused tool schemas being sent anyway?&lt;/li&gt;
&lt;li&gt;Are retrieved documents narrowed to only the chunks needed right now?&lt;/li&gt;
&lt;li&gt;Are retries resending giant prefixes that changed just enough to miss cache reuse?&lt;/li&gt;
&lt;li&gt;Is this automation delayed enough that Anthropic's 5-minute cache window probably won't help?&lt;/li&gt;
&lt;li&gt;Is this Gemini request even large enough to qualify for implicit caching?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That checklist has saved me more money than most model-routing debates.&lt;/p&gt;
&lt;h2&gt;
  
  
  When model switching actually does matter
&lt;/h2&gt;

&lt;p&gt;Model choice still matters.&lt;/p&gt;

&lt;p&gt;If you've already cleaned up context and a cheaper model can do the same job, switch models.&lt;/p&gt;

&lt;p&gt;That can be a clean win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classification on Gemini Flash&lt;/li&gt;
&lt;li&gt;extraction on a smaller open model&lt;/li&gt;
&lt;li&gt;simple branch logic on Qwen or Llama&lt;/li&gt;
&lt;li&gt;premium reasoning only where it actually matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I would do that after prompt cleanup.&lt;/p&gt;

&lt;p&gt;If the prompt is bloated, you're just moving the same garbage to a cheaper truck.&lt;/p&gt;
&lt;h2&gt;
  
  
  One shell-level habit that helps
&lt;/h2&gt;

&lt;p&gt;If you run your agents as services, log request size and retry count together.&lt;/p&gt;

&lt;p&gt;Something as simple as this can surface the real cost pattern fast:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"prompt_payload_chars&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;retry_count&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;workflow_step"&lt;/span&gt; app.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or aggregate it properly in your normal observability stack.&lt;/p&gt;

&lt;p&gt;The point is the same: don't just track model name. Track how much context each step is dragging around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Standard Compute fits
&lt;/h2&gt;

&lt;p&gt;This is also why flat-rate inference is appealing for agent builders.&lt;/p&gt;

&lt;p&gt;If you're running lots of automations across n8n, Make, Zapier, OpenClaw, or custom workers, per-token pricing punishes every messy workflow habit.&lt;/p&gt;

&lt;p&gt;Standard Compute takes a different approach: one predictable monthly price, OpenAI-compatible API, and routing across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20.&lt;/p&gt;

&lt;p&gt;That doesn't mean prompt compaction stops mattering. It still improves latency, quality, and throughput.&lt;/p&gt;

&lt;p&gt;It just means you're not babysitting token spend every time an agent retries or a workflow fans out.&lt;/p&gt;

&lt;p&gt;For teams running agents all day, that tradeoff is pretty compelling.&lt;/p&gt;

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

&lt;p&gt;If your agent bill feels weirdly high, don't start with model shopping.&lt;/p&gt;

&lt;p&gt;Start by auditing context.&lt;/p&gt;

&lt;p&gt;The usual waste is not mysterious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full chat history&lt;/li&gt;
&lt;li&gt;oversized tool schemas&lt;/li&gt;
&lt;li&gt;repeated document dumps&lt;/li&gt;
&lt;li&gt;memory that keeps growing between steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do prompt compaction first.&lt;br&gt;
Then reduce context-window waste.&lt;br&gt;
Then optimize model selection.&lt;/p&gt;

&lt;p&gt;The fastest way to cut agent spend is often not finding a cheaper model.&lt;/p&gt;

&lt;p&gt;It's teaching your workflow to shut up.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
