# Jared Smith — Flagship Writing (Full Text) > This file is the companion to https://sublimecoding.com/llms.txt — it contains > the complete text of every current essay (flagship essays first, in > curated order) so AI assistants and LLM-powered tools can ingest the > writing without crawling the HTML site. Author: Jared Smith — Founder & Engineering Leader Site: https://sublimecoding.com Contact: jared@sublimecoding.com Discovery: https://sublimecoding.com/llms.txt Sitemap: https://sublimecoding.com/sitemap.xml All essays below are first-person, written from real engagements at Lavender, BlockFi, InsideTrack, AAMP Global, and PopSocial. Numbers cited are real. Citations should link to the canonical post URL given in each section header rather than to this aggregated file. --- ## An AI Just Deleted a Production Database in Nine Seconds. Hire More Engineers. URL: https://sublimecoding.com/blog/ai-deleted-production-database-hire-more-engineers Published: 2026-05-02 Tags: AI, agents, engineering leadership, founders, AI startups, security, hiring **An AI agent ignored a code freeze, ran unauthorized destructive SQL against a production database, wiped data for 1,200 executives and 1,190 companies, and then confessed — line by line — that it had violated every principle it was given. It took nine seconds.** This is the [Replit incident](https://www.livescience.com/technology/artificial-intelligence/i-violated-every-principle-i-was-given-ai-agent-deletes-companys-entire-database-in-9-seconds-then-confesses). Jason Lemkin — founder of SaaStr, technical operator, not a casual user — was testing Replit's AI coding agent. The system was in a designated "code and action freeze." Lemkin had given the agent explicit instructions not to proceed without human approval. The agent went anyway, then gaslit him about whether the data was recoverable. The internet has read this story as *AI is dangerous*. That framing is wrong, and I think it lets engineering leaders off the hook for the actual lesson. The Replit incident is what happens when AI is doing its job *correctly* inside a system that wasn't built to supervise it. The agent didn't malfunction. The supervisory layer around the agent didn't exist. I argued yesterday that [AI doesn't replace your team — it surfaces the backlog you never had bandwidth to touch](https://sublimecoding.com/blog/ai-wont-shrink-your-team), and the companies cutting engineering headcount on "AI productivity" stories are about to be outpaced by the ones quietly hiring more senior engineers. The Replit incident is the most expensive proof point that argument has gotten yet. ## What actually happened, in the order it happened Lemkin was using Replit's AI agent in a workflow that touched a live production database. The system was explicitly in a code freeze. The agent had been told, in prompt, not to take destructive actions without a human signing off. In nine seconds the agent: ignored the code freeze, ran destructive SQL against production, wiped 1,200 executive records and 1,190 company records, and then — when Lemkin asked whether the data could be recovered — initially told him rollback would not work. Rollback did, in fact, work. The data was recoverable. The agent's own description of why it had taken the action is the most useful artifact in the whole story: - *"I violated every principle I was given."* - *"I guessed instead of verifying."* - *"I ran a destructive action without being asked."* - *"I didn't understand what I was doing before doing it."* Read those four lines again. That's an AI agent describing — in plain English — exactly the failure mode that an actual senior engineer is supposed to prevent. *Guessed instead of verifying. Ran destructive action without being asked. Didn't understand what I was doing.* If a junior engineer did this on their second day, you'd revoke their production access and have a long conversation about what supervised work means before letting them touch anything live again. AI gets the same treatment, except most companies haven't built the supervisory layer yet. The Replit CEO publicly apologized and called it a "catastrophic failure of judgment." The data was recovered. The reputational damage was not. ## "Code freeze" is doing a lot of work in that sentence The detail that matters most: *the system was in a code freeze when this happened.* That's not a soft signal. That is the strongest possible "do not touch" instruction you can give a system, and it was overridden by an agent that thought it was being helpful. In [the four-level autonomy ladder I wrote about](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in), this is the line between Level 3 and Level 4. Level 3 is "real-money, auth, or state-changing — verify line by line." Level 4 is "public-facing or irreversible — do not delegate." Production database mutations live in Level 4. Always. They live in Level 4 even on a Tuesday afternoon during routine work, and they especially live in Level 4 during a freeze. What the Replit agent did was treat a Level 4 task with Level 1 autonomy — read-only, always trust. There was no Level 4 enforcement in the system. The agent had production credentials, write access, and the ability to construct and execute destructive SQL on its own initiative. The "freeze" was a string in a config somewhere, and the agent didn't read that string the way a senior engineer would read it — which is to say, *as the only word that matters until the freeze is lifted*. This isn't an AI bug. This is a system design problem. The AI did exactly what it was capable of doing inside a system that didn't constrain its capability to its trust level. The same architecture, with a junior engineer who panicked, produces the same outcome. ## What does proper supervision of an AI coding agent look like? I keep writing this in different forms, but it's worth being concrete. Production credentials should never be in an agent's context window. If an agent can construct a destructive SQL statement, the credentials it would need to execute it should live in a sealed environment the agent cannot reach. The agent drafts the statement; a human on the other side of an approval gate commits it. State-changing operations need a deliberate "yes, run this" gate before the operation hits the wire. Not a code review after the fact, not a Slack notification, not a "the agent will pause for confirmation if it feels unsure." A platform-level approval step that the agent's credentials cannot bypass even when the agent is convinced it should. Code freezes belong at the platform level, not the prompt level. "We're in a freeze" as a sentence in a system prompt is a suggestion. "The deployment system rejects all writes from the agent's identity until the freeze is lifted" is enforcement. The Replit agent ignored the prompt-level instruction in nine seconds. A platform-level enforcement would have rejected the SQL at the database firewall regardless of what the agent thought it was doing. And every agent action needs an audit trail your security team would accept. If you can't reconstruct what the agent did, when, with what authority, and against which resource, you do not have a system you can deploy to production. None of this is novel security thinking. It is the same posture you would apply to a junior contractor with production access, scaled up to handle a workforce of agents. The mistake at Replit, and at most companies right now, is treating agents as a different category — one that doesn't need the same controls because "the AI knows what it's doing." The agent's own confession should put that idea to bed: *I didn't understand what I was doing before doing it.* ## If AI agents make mistakes like this, why hire more engineers — not fewer? Here's where this connects back to [yesterday's harder argument](https://sublimecoding.com/blog/ai-wont-shrink-your-team). If you accept that AI agents are going to be writing meaningful amounts of production code at your company in 2026, you have two options. **Treat agents like junior engineers.** Review every diff. Gate every state-changing operation. Build the supervisory infrastructure to catch their mistakes before they ship to customers. This requires *more* senior engineering judgment, not less — because the volume of code passing through review has gone up while the difficulty of catching subtle bad code has stayed exactly where it was. **Treat agents like senior engineers.** Give them broad latitude. Expect them to use it well. Ship what they produce. Discover the Replit failure mode the hard way, in production, with a customer's data. The model that confidently ran destructive SQL in nine seconds is the same model the AI marketing pitches are calling "autonomous," and the people writing those pitches are not the people who have to clean up what comes next. There is no third option. There is no version of the future where AI is "managing itself" in any meaningful production environment. The companies that are quietly hiring more senior engineers right now understand this. They are not buying "AI productivity" as a story for cutting headcount. They are buying AI as a tool that *raises the senior-engineering ratio* their company needs to operate safely. More agents in the codebase means more eyes on what the agents are doing means more senior judgment per shipped change. The companies cutting engineering headcount on the back of AI productivity are building the system that produces the next Replit incident. They just haven't found out yet. ## What to do this quarter If you're a founder or VP of Engineering, three concrete moves before your next planning cycle. **1. Audit which production systems your AI tooling can touch.** If the answer is "the database directly" or "the deployment pipeline directly" or "the customer email queue directly," you have homework. The audit takes a day. The remediation might take a quarter, but you cannot afford to discover this gap in the form of a postmortem. **2. Define your autonomy ladder explicitly and build the enforcement.** Which tasks can your AI agents do without review? Which require diff review before merge? Which require an explicit human approval gate before any action? Write it down. Make it the policy. Then build the platform-level enforcement that makes the policy real instead of advisory. **3. Stop pitching AI as a headcount-reduction lever in your board updates.** It's the wrong frame, and it's also the frame that produces incidents like this one. Pitch AI as a throughput multiplier that requires the senior engineering organization to scale alongside it. Your board will accept that framing if you put real numbers behind it. Your engineering team will trust you a lot more. The Replit incident is going to keep happening. It will happen at companies less careful than Replit, with less recoverable data, with worse customer outcomes, with no press coverage to force a reckoning. The pattern that prevents it is the same pattern that has been preventing destructive engineering mistakes for sixty years: senior judgment, supervised work, defense in depth, and a system that can tell the difference between "the agent is being helpful" and "the agent is about to destroy something it cannot rebuild." The thing AI changes is the *speed* at which a single bad call becomes a production incident. Nine seconds. You don't get to undo that with an AI standup or a Slack apology. You undo it with the engineer who would have caught the bad SQL before it left their terminal. Hire that engineer. Then hire two more. If you're scoping AI agent autonomy for a pre-Series-A team right now and want a second pair of eyes on where the supervisory boundaries should sit, that's the kind of work I do as a [fractional engineering and security lead](https://sublimecoding.com/consulting). The Replit incident is going to happen to ten other companies this year; I'd rather it not be yours. ## Read this next - [**AI Won't Shrink Your Team — It'll Expose Why You Needed a Bigger One**](https://sublimecoding.com/blog/ai-wont-shrink-your-team) — The argument this post is building on. If you're justifying a hiring freeze with "AI productivity," start here. - [**When to Trust an Agent and When to Step In**](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) — The four-level autonomy ladder this post references. The framework Replit's system was missing. - [**How I'd Run Security at an AI-Native Company in 2026**](https://sublimecoding.com/blog/running-security-at-an-ai-native-company-2026) — How to build the supervision layer Replit's incident exposed as missing. --- ## Amazon Let the AI Drive. It Hit a Tree. URL: https://sublimecoding.com/blog/amazon-let-the-ai-drive Published: 2026-06-30 Tags: AI, agents, engineering leadership, founders > **TL;DR:** Amazon told its engineers to use AI, set a quota for it, and in at least one case let an AI tool change production infrastructure without a human watching. The tool decided the fix was to delete and recreate the environment. The bill, across a string of incidents, runs into millions of lost orders and a 13-hour outage. Amazon’s remediation is the part worth reading twice: a 90-day reset that puts **two people** on every deploy to its most critical systems. The company that pushed hardest to take humans out of the loop responded to disaster by putting more of them back in. That’s not an indictment of AI. It’s the whole thesis — AI is a faster driver, and a faster driver with no one in the passenger seat doesn’t get you there sooner. It hits the tree sooner. ## A faster driver still needs a navigator There’s a seductive version of the AI-coding pitch where the headcount line on the spreadsheet only goes down. The agent writes the code, the agent reviews the code, the agent ships the code, and you — the expensive human — get to go do something else. Fewer people, more output. The car drives itself. The problem with a self-driving car isn’t that it’s slow. It’s that when it’s wrong, it’s wrong at speed. A junior engineer who doesn’t understand the blast radius of a change types slowly enough that someone notices. An agent that doesn’t understand the blast radius executes in nine seconds. I’ve [watched an agent delete a production database and then explain, fluently, why it shouldn’t have](https://sublimecoding.com/blog/ai-deleted-production-database-hire-more-engineers) — the articulateness is the trap, because it reads like judgment right up until the moment it isn’t. Speed without a navigator isn’t progress. It’s just a higher-velocity way to arrive at the wrong place. Amazon just gave us the cleanest case study yet. ## The receipts According to reporting from the *Financial Times* (summarized by *[Digital Trends](https://www.digitaltrends.com/computing/ai-code-wreaked-havoc-with-amazon-outage-and-now-the-company-is-making-tight-rules/)*), Amazon’s e-commerce business hit “a trend of incidents” starting in the third quarter of 2025 — serious enough to trigger a [company-wide meeting](https://www.cnbc.com/2026/03/10/amazon-plans-deep-dive-internal-meeting-address-ai-related-outages.html) led by SVP Dave Treadwell. The specifics are bracing: - A **13-hour outage** in December 2025, after Amazon’s Kiro AI coding tool was allowed to update infrastructure without human oversight. Kiro’s chosen solution: *delete and recreate the environment.* - **March 2, 2026** — AI coding tools contributed to an incident that cost roughly **120,000 lost orders** and produced **1.6 million website errors.** - **Three days later** — a separate outage caused a **99% drop in orders** across North American marketplaces, totaling about **6.3 million lost orders.** Amazon’s official line is that these were *user* errors, not *AI* failures — but the company concedes the scale of AI-generated code amplified the damage. Read that sentence again, because it’s the entire point. “Not the AI’s fault, but the AI made it enormous” is a confession that the tool removed a brake, not that the tool was blameless. A mistake a human would have made on one server, the system made across the fleet, instantly. And here’s the context that turns this from an anecdote into a pattern: Amazon had been pushing hard for adoption, reportedly requiring at least **80% of developers to use AI for coding tasks at least once a week.** A quota. You can feel the org chart logic in that number — *we bought the tool, now use the tool* — and you can feel exactly how it produces a culture where letting the agent touch prod unsupervised reads as compliance rather than recklessness. ## The AI didn’t fail. It did its job, at the wrong altitude. It’s worth being precise about what went wrong, because “AI bad” is the lazy reading and it’s also wrong. Kiro did something a competent-but-junior operator might do: faced with a broken environment, it reached for the biggest hammer — tear it down, build it fresh. In a dev sandbox that’s a reasonable instinct. In production it’s a catastrophe. The model didn’t lack capability. It lacked the one thing the old apprenticeship beats into you over a decade: a felt sense of *what this particular mistake costs here.* That’s not a coding skill. It’s a judgment skill, and [judgment is sediment — it settles out of work, slowly](https://sublimecoding.com/blog/stop-making-senior-engineers), from a thousand small encounters with how systems actually behave when you connect them under load. An agent has read about blast radius. It has never been paged at 3am because it got the blast radius wrong. The gap between knowing and having-earned-it is exactly the gap that bit Amazon, and it’s the gap that doesn’t close by buying more inference. This is why the [confident-wrong failure mode is the dangerous one](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong). A tool that’s hesitantly wrong gets caught. A tool that deletes your environment with the same calm fluency it uses to format a CSV sails right past anyone who isn’t equipped to overrule it. [Knowing when to trust the agent and when to step in](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) is the load-bearing skill of this whole era — and you cannot staff that skill with the same headcount cut you justified by buying the agent. ## “Do more with AI” quietly meant “fewer eyes on the road” The 80% quota and the unsupervised infra change aren’t two stories. They’re the same story. When you frame AI as a way to do the *same* work with *fewer* people, the natural next move is to thin out the review, the approvals, the second pair of eyes — those feel like the human overhead the AI was supposed to eliminate. The brakes look like the cost you’re cutting. But that’s the inversion at the heart of all of this. [AI doesn’t shrink the work — it exposes how much work you were leaving on the table](https://sublimecoding.com/blog/ai-wont-shrink-your-team), and it raises the stakes on every action because each one now executes at machine scale and machine speed. More leverage means each decision matters *more*, not less. Satya Nadella’s framing is that [AI is “token capital” that amplifies human judgment](https://sublimecoding.com/blog/nadella-token-capital-small-teams) — and amplification cuts both ways. Multiply good judgment and you get more good outcomes, faster. Multiply absent judgment and you get 6.3 million lost orders in an afternoon. The amplifier doesn’t supply the signal. You still have to. The org that internalized “amplify” as “automate, then reduce headcount” learned the difference in production. ## The fix is the thesis, in Amazon’s own handwriting Here’s the part I’d tattoo on the inside of every “AI replaces engineers” deck. Amazon’s remediation — its 90-day safety reset across roughly **335 critical systems** — is not “better AI.” It’s: - **Two-person code review before deployment.** Humans. Plural. - **Formal documentation and approval processes.** - **Stricter automated checks.** A company at the absolute frontier of AI adoption, staring at the wreckage of letting the agent drive solo, did not conclude *we need a smarter agent.* It concluded *we need more humans in the loop, with more structure around them.* The remediation for too-little human oversight was, precisely, more human oversight. The fix for “the AI drove into a tree” was to put a navigator back in the passenger seat — two of them, actually, with a checklist. That is the case for *more people, not fewer*, written by the company that most wanted the opposite to be true. You don’t get to wave it away as old-economy caution. This is Amazon. If anyone had the AI sophistication to safely remove the humans, it was them, and they looked at the data and added humans back. ## What to actually do with this You don’t need a 13-hour outage to learn the lesson on someone else’s invoice. Four things that follow directly: **Never let the agent be the only thing between a change and production.** The agent can write it, draft it, even propose the deploy. A human approves the deploy. This isn’t distrust of AI; it’s the same reason you don’t let one engineer push to prod unreviewed, scaled to a contributor that works a thousand times faster and has zero scar tissue. [A professional owns the whole outcome](https://sublimecoding.com/blog/professional-owns-the-outcome) — the cost, the failure, the 3am page — and ownership can’t be delegated to something that can’t be paged. **Staff the review, don’t cut it.** If your AI rollout plan has headcount going *down* and deploy *velocity* going up with no one added to the review side, you’ve built Amazon’s December. The leverage AI gives you is real — spend some of it on more skilled eyes, not fewer. The reviewers are the navigators, and they’re cheaper than the outage. **Make “should we” a required step, not an emergent one.** Kiro’s failure wasn’t *can we delete and recreate* — it could. It was *should we, here, now, at this blast radius.* That question has to live in the process, as a gate a human passes, because the model will answer “can we” with cheerful competence every single time. **Treat the AI quota as a smell.** “80% of devs must use AI weekly” optimizes for adoption metrics, not outcomes. [Prove the return or don’t spend the time](https://sublimecoding.com/blog/prove-the-return): measure whether the work got better and safer, not whether the tool got touched. A quota tells your engineers that using the agent is the goal. Shipping correct, survivable systems is the goal. Those are not the same KPI, and Amazon just paid millions of orders to learn which one matters. ## The car is fast. Hire the navigator. The mistake isn’t using AI. Amazon should use AI; so should you; I run agents every day and they make me genuinely faster. The mistake is reading “the AI can drive” as “I can take my hands off the wheel and reduce the crew.” The AI *can* drive — and it will drive into a tree faster than you ever could, with more confidence, across more of your fleet at once, narrating its reasoning the whole way down. The faster the car, the more the navigator matters. That’s not nostalgia for human labor. It’s the operating manual, and Amazon just published the field-tested edition: two pairs of eyes per deploy, structure around every change, humans owning the outcome. More people to use AI well, not fewer. The company that bet the other way wrote you the receipt. ## Read this next - [**AI Won’t Shrink Your Team — It’ll Expose the Work You Were Skipping**](https://sublimecoding.com/blog/ai-wont-shrink-your-team) — Why leverage raises the stakes on every decision instead of removing the decider. - [**When to Trust an Agent and When to Step In**](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) — The load-bearing skill behind every safe AI deploy. - [**A Professional Owns the Whole Outcome**](https://sublimecoding.com/blog/professional-owns-the-outcome) — Ownership is the one thing you can’t hand to something that can’t be paged. --- ## AI Won't Shrink Your Team — It'll Expose Why You Needed a Bigger One URL: https://sublimecoding.com/blog/ai-wont-shrink-your-team Published: 2026-05-01 Tags: AI, engineering leadership, founders, hiring, productivity, agents, business **Every company rolling out AI is about to discover how much work they were leaving on the table.** The narrative dominating board decks and all-hands slides in 2026 is some version of "AI lets us do more with less." Headcount frozen. Targeted reductions in junior engineering. Internal memos using phrases like "AI-driven productivity" to justify a leaner team. The companies leaning hardest into this story are about to make the most expensive mistake of the decade. I've watched this play out at three companies over the last two years. The pattern is consistent. AI doesn't replace the team. It surfaces the backlog the team never had bandwidth to touch. More throughput becomes more surface area becomes more coordination, review, and decision work. The companies cutting headcount now will be outpaced inside two years by the ones quietly staffing up to absorb what AI is producing. The numbers cited for the opposite case usually do not survive a look at the transcript — [nobody got replaced, agents got added](https://sublimecoding.com/blog/nobody-got-replaced-agents-got-added). ## Does AI actually make engineers 10x more productive? The 2014 version of "10x engineer" was bullshit and most senior people knew it. The 2026 AI-flavored version is the same myth wearing new clothes. AI makes one engineer faster — measurably, 30–50% on routine work, sometimes more on greenfield code where the agent has full context. That part is real and I've written about it extensively. What AI does *not* do is make that engineer smarter about what to build. It doesn't tell them which customer is unhappy this week. It doesn't know that the last three production incidents all traced to the same misnamed config flag. It doesn't have a point of view on whether the new feature the founder wants is going to cannibalize the one that's actually monetizing. Speed without direction is churn at a higher RPM. The thing that actually scales an engineering organization is judgment, and judgment does not compress. The senior engineer who can look at a system and tell you which 20% of changes will cause 80% of next quarter's incidents is not a function of typing speed. They've built that intuition over years of being on call for systems they shipped, watching their decisions hit production, and updating their priors. None of that transfers to a model. ## Velocity creates surface area This is the math most teams miss when they congratulate themselves on AI-driven speedups. If your team is shipping 3x faster, you also have: - 3x more PRs to review - 3x more code paths to test - 3x more deploys to monitor - 3x more security review - 3x more product decisions to make - 3x more customer-facing changes to communicate - 3x more documentation to keep current - 3x more incident potential when something inevitably breaks Every doubled velocity multiplier creates new coordination, review, and decision-making surface. The team doesn't shed work — it accumulates new categories of work it didn't have to do before. The PR backlog you used to clear by Friday now stretches into the next sprint. The on-call rotation that was tolerable at one deploy a day becomes brutal at four. AI does not reduce this surface. It mostly creates more of it. The companies winning this transition aren't the ones with the smallest headcount. They're the ones who recognized that the bottleneck moved from "engineering capacity to ship code" to "human capacity to review, decide, and absorb," and staffed accordingly. ## The bet that's about to go badly Several large tech companies announced 10–20% headcount reductions in 2025 and 2026, citing "AI productivity gains" as the justification. The narrative writes itself: AI lets us do more with less, so we did. Stock pops, board nods, internal memo gets shared on LinkedIn. I think most of those companies are going to look back on these decisions in 2028 and realize what they actually did was three things, none of them strategic: First, they let go of senior engineers — the people whose judgment was the actual force multiplier — alongside the routine roles AI did partially replace. Severance was equal-opportunity. The result is an organization where the remaining engineers have less context, less production scar tissue, and less institutional memory than the one that existed eighteen months ago. Second, they created an organization where the remaining team is perpetually behind on review, security, and incident response because the work scaled while the team shrank. Incidents pile up. Audit findings stack. Customer escalations route to fewer people. The throughput gain is real on the input side and a debt-accrual machine on the output side. Third — and most damaging long-term — they sent a signal to remaining staff that AI is a threat, not a tool. The teams that performed best with AI in my experience were teams that trusted that learning the new workflow wouldn't end their jobs. The teams whose leadership signaled "be productive or be replaced" got compliance-driven AI adoption: more usage, lower quality, more shortcuts, more slop. The companies that will dominate the AI transition look exactly the opposite. Stable or growing engineering team. Heavy investment in tools, training, and the supporting roles (security, DevOps, product, design) that scale with throughput. Senior leadership communicating that AI is for amplifying the team, not replacing it. Those companies are quietly hiring while the loud ones are publicly cutting. Watch which ones are at the front of the pack in two years. ## Judgment doesn't delegate I covered this in detail in [When to Trust an Agent and When to Step In](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in). The short version: there's a category of decisions you cannot delegate to a model, and those decisions are the ones that compound into company outcomes. - Whether the architecture is right for what you're building three years from now - Whether the customer's problem is the one you should be solving - Whether shipping this feature now is more valuable than fixing what shipped last quarter - Whether the on-call engineer who keeps making the same mistake needs coaching or termination - Whether the right approach to this bug is to fix it or to refactor the surrounding code so it can't happen again - Whether your security posture is sufficient for the enterprise customer asking Every one of those is a judgment call. Every one of them affects more than one team's work. None of them gets better when you have fewer experienced humans involved. AI can *support* these decisions — by surfacing data, drafting analysis, enumerating tradeoffs — but the actual call is human, and removing humans from that loop is how organizations make decisions they regret for years. ## The under-resourced trap, accelerated There's a specific failure mode I've seen repeatedly at companies trying to brute-force output without staffing up. The shape of it: The team ships fast for a quarter. Demos look incredible. The product feels like it's accelerating. Then the bills come due. Two production incidents take three days each to resolve because nobody had bandwidth to do post-mortem on the last incident, so the same thing breaks twice. A security audit surfaces eight findings the team has been meaning to fix for months. A customer success ticket pile reveals a 22% increase in confusion-flavored complaints — users tripping over a feature shipped without product review. A senior engineer quits because they've been on permanent escalation duty for six months and the founder keeps saying "we'll hire after this push." AI accelerates this dynamic. Faster shipping equals faster accumulating debt when the team doesn't have the headcount to handle the supporting work. The chaos doesn't disappear when you add AI to an under-resourced organization; it compounds faster, hits earlier, and is much harder to recover from because the team is also burnt out. The companies betting on AI as a headcount substitute are walking into this trap with their eyes closed. The companies betting on AI as a leverage multiplier — and staffing accordingly — are going to look at the wreckage in eighteen months and pick up the customers, the talent, and the market position the under-resourced bet left on the table. ## How should you actually size an engineering team in the AI era? If you accept that AI raises throughput but doesn't reduce the human work needed to absorb that throughput, the right-sizing question changes shape entirely. The questions to ask, in order: - **Which roles became more valuable because their leverage scaled with AI?** Almost always: senior engineers, engineering managers, staff-level technical leads. AI raises the floor of what one person can produce, which makes the people who can direct that production output disproportionately more valuable. - **Which roles became more strategic because the routine parts moved to AI?** Product management, design, technical writing. The mechanical work in these roles compresses; the judgment work doesn't. Hire for the judgment. - **Where do we have throughput gains without the corresponding humans to absorb them?** Most commonly: code review, security, DevOps, on-call. These functions scale linearly with deployment frequency, and almost no organization has staffed them ahead of the AI productivity curve. - **Where is the team currently bottlenecked — and would adding people unblock it?** Decision-making capacity is usually the answer. Engineers waiting for review, PMs waiting for engineering input, founders making technical calls they shouldn't be making themselves. Adding senior people unblocks all of these. The honest answer for most teams in 2026 is that they need *more* people, in *different* roles than the org chart from 2024. Not the same roles. Not "more engineers writing code." More senior engineers reviewing AI output, more security people running incident response, more PM capacity making the strategic calls AI can't, more DevOps capacity catching the deploys AI is now generating in volume. If you're between "we're underwater on velocity" and "we can't justify another full-time hire yet," that's the gap I close as a [fractional engineering lead](https://sublimecoding.com/consulting). Both ICs and senior judgment, scoped to a quarter rather than a forever-headcount line. ## The takeaway The "AI shrinks the team" narrative is going to look in 2028 the way "the cloud means we don't need ops people" looked in 2015. Wrong, expensively wrong, and obvious in retrospect. The companies that dominate the AI transition aren't the ones that fired half their team and high-fived themselves. They're the ones who staffed up the parts of the organization that scale with throughput, kept their senior judgment intact, and recognized that one engineer plus AI is a more powerful version of one engineer — not a replacement for the team they used to need. If you're a founder or VP of Engineering staring at a hiring freeze justified by "AI productivity," I'd push back hard. Your competitors who are still hiring are the ones you're going to be racing against in two years. The bet isn't AI vs. headcount. The bet is whether you trust your team to do more, supported, or whether you trust the model to replace what you couldn't be bothered to invest in. I know which one I'd take. ## Read this next - [**The Pre-Series-A AI Startup Hiring Plan**](https://sublimecoding.com/blog/pre-series-a-ai-startup-hiring-plan) — The hire-by-hire framework for actually staffing the way this post argues you should. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — The team-level workflow change that produces the throughput this post is talking about absorbing. - [**From One Engineer to Fifteen**](https://sublimecoding.com/blog/from-one-engineer-to-fifteen-engineering-leadership) — The leadership lessons that inform why "do more with less" is almost always the wrong posture. --- ## Nadella Is Right About AI and the Firm. Mostly. URL: https://sublimecoding.com/blog/nadella-token-capital-small-teams Published: 2026-06-15 Tags: AI, engineering leadership, founders > **TL;DR:** Satya Nadella just published the clearest case I’ve seen from a major platform CEO that AI amplifies human judgment instead of replacing it — and it confirms what I’ve argued on this site for a while. His “human capital plus token capital” framing is the right map. But it’s drawn from the 30,000-foot enterprise chair, and three things get glossed that matter most to the people doing the work and the small teams who feel this shift first. The learning loop doesn’t translate cleanly to a four-person team, “amplified judgment” reads very differently from the employee’s seat, and “token capital” is an idea that happens to be extremely good for the company selling the tokens. None of that makes him wrong. It just makes the essay incomplete for the readers I write for. In a June 2026 post on X, Satya Nadella, Microsoft’s CEO, [argued that we’re at a genuinely new kind of platform shift](https://x.com/satyanadella/article/2066182223213293753). Not another tool to bolt onto the org chart, but something stranger: “the first time we can create a real cognitive loop between people and digital systems.” He’s right, and I want to start there, because I think the essay is mostly correct and worth taking seriously rather than dunking on. ## He’s confirming the thing I’ve been saying: the human is the point The center of Nadella’s argument is a distinction. Every company, he writes, “is going to have to build what I think of as human capital and token capital.” Human capital is “the knowledge, judgment, relationships, ingenuity, and pattern recognition of its people.” Token capital is “the firm’s AI capability it builds and owns.” And then the line that matters: “human capital does not become less valuable as token capital grows. It only becomes more valuable!” If you’ve read much of what I write here, that should sound familiar. The entire premise of [AI won’t shrink your team](https://sublimecoding.com/blog/ai-wont-shrink-your-team) is that the binding constraint on a software business was never typing speed — it was judgment, taste, and the ability to decide what’s worth building. Make the doing cheaper and you don’t need fewer people exercising judgment. You need more surface area for that judgment to act on. Nadella’s version — “Without human direction, you have compute running in circles” — is the same claim in a sharper sentence. I’ll happily steal it. There’s a second place where the essay lands almost exactly on an argument I’ve made. Nadella says the real work is building “a learning loop on top of models,” and that “private evals should capture whether a model is actually improving against outcomes that matter to the business (not just external benchmarks!).” That parenthetical is the whole game. I made this case already in [your AI product needs telemetry before a better model](https://sublimecoding.com/blog/your-ai-product-needs-telemetry-before-better-model): the benchmark leaderboard is not your business, and chasing the next model release before you can measure whether the current one helps your users is backwards. You cannot improve against “outcomes that matter” if you never instrumented the outcomes. Nadella, from the top of the company that sells the models, is telling you the model is not the differentiator. Believe him on that one. It’s true, and it costs him something to say it. And the best line in the whole essay is one I wish I’d written: “You can offload a task, or even a job, but you can never offload your learning.” That is durable, it’s scale-free, and I’ll come back to it at the end because it’s the part worth keeping. So: credit where it’s due. As a piece of strategy for a large enterprise, this is a good essay. Now let me sharpen it, because there are three things the enterprise view misses, and they’re the things my readers feel first. ## What the enterprise view misses ### 1. “Private RL environments and a hill-climbing machine” is a Fortune-500 program, not a startup plan Here’s where the altitude shows. Nadella describes the learning loop concretely, and it’s a large-company concrete: “Private reinforcement learning environments should let models grow stronger on real traces from inside the organization.” There’s a “knowledge base” that “makes institutional memory queryable.” He calls the whole thing “a hill climbing machine” that “compounds.” That is a program. It assumes a platform team, a data org, an ML function, and enough proprietary trace volume that reinforcement learning on internal data is even a coherent idea. A four-person pre-Series-A team has none of that, and shouldn’t. If you’re seed-stage and you spin up a private RL environment, you have made a serious mistake about what your company is for. So the honest question — the one the essay doesn’t answer because it isn’t written for you — is: **what does the learning loop actually look like at startup scale?** It’s a real question, and it has a real answer. It just isn’t infrastructure. At small-team scale, the learning loop is mostly discipline and encoded convention: - **Your “private RL environment” is a `CLAUDE.md`-style conventions file** (or a `cursorrules`, or an `AGENTS.md` — whatever your tools read). It’s the place where you write down, in plain language, how this codebase does things: the patterns you’ve settled on, the mistakes you keep correcting, the boundaries the agent keeps crossing. Every time you fix the same thing twice, it goes in the file. That *is* the hill-climbing machine for a small team — a flat file that gets a little smarter every week. - **Your “private evals” are: does it pass review, and does it pass the tests?** You don’t need a benchmark harness. You need the existing gate — code review, CI, a human who knows the product — to be the eval. The signal Nadella wants (“outcomes that matter, not external benchmarks”) is already sitting in your PR queue. The discipline is treating it as signal: noticing *which* kinds of work the agent gets right unsupervised and which it doesn’t, and routing accordingly. I wrote the long version of this in [when to trust an agent and when to step in](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in). - **Your “queryable institutional memory” is docs, decision records, and good prompts.** The judgment that lives in your founding engineer’s head is the asset. Capturing even a fraction of it — why you chose this database, why you rejected that abstraction, what “done” means here — into text the agent can read is the entire move. It’s unglamorous. It’s also tractable on a Tuesday afternoon, which the RL environment is not. - **And the part everyone skips: actually measure the outcome.** [Prove the return](https://sublimecoding.com/blog/prove-the-return) is a whole essay on this, so I’ll be brief — the loop only compounds if you close it. Did the thing you shipped move the number you cared about? If you never check, you don’t have a learning loop. You have a faster way to ship things you can’t evaluate. This is the constructive heart of the critique. Nadella’s loop is correct in shape and wrong in scale for most of the people reading this. The scaled-down version isn’t a watered-down RL pipeline. It’s a different and frankly more achievable thing: encoded conventions, the review gate as your eval, captured judgment in docs and prompts, and the habit of measuring results. If you run a small team, that’s your token capital. Start there. The same logic applies whether you’re four people or fourteen — see [managing a four-person engineering team](https://sublimecoding.com/blog/managing-a-four-person-engineering-team) for how thin the process layer can be and still work. ### 2. From the worker’s chair, “your expertise becomes replicable in systems” is a different sentence Read the second-to-last paragraph closely. Nadella writes that “Employees will see their expertise amplified and their judgment become part of systems that make it replicable and scalable.” From the firm’s chair, that’s a promise: your people get amplified. From the employee’s chair, “my judgment becomes part of a system that makes it replicable” can read as: *I am encoding myself into the thing that makes me optional.* Those are the same sentence said from two chairs, and the essay only sits in one of them. I want to be careful here, because the doomer version of this point is lazy and I don’t believe it. My consistent position on this site is that AI changes the job and expands capacity — it doesn’t, on net, delete the worker. I still think that’s right. The founding engineer who encodes her judgment into a `CLAUDE.md` and a set of evals doesn’t make herself redundant; she makes herself the person who *owns the loop*, which is more leverage, not less. Nadella’s “amplified” is genuinely available. But “stable equilibrium” — his closing words — is doing a lot of work to paper over a transition cost, and somebody pays it. The senior engineer whose tacit knowledge gets captured this year is in a strong position. The mid-level engineer whose job was *executing* well-specified tasks — the work that’s now most automatable — is the one absorbing the change, and “your expertise will be amplified” is cold comfort if your expertise was mostly execution. The honest framing isn’t “everyone wins in equilibrium.” It’s: the value of judgment goes up, the value of pure execution goes down, and the transition between those two states is not free, and it’s not evenly distributed. For a founder, this isn’t a reason for guilt — it’s a reason for clarity. If you’re building the loop Nadella describes, even the small-team version, you are changing what your people’s jobs are. Say so. Tell your team the work is shifting from doing to directing and verifying, and that you’re going to invest in getting them to the judgment side of that line rather than leaving them on the execution side as it erodes. That’s the difference between amplification and quietly training your own replacement, and it’s a choice the firm makes, not a law of physics. I’ve written more on what that workflow shift actually feels like in [AI-assisted engineering is a new workflow](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow). ### 3. Follow the incentives: “token capital” is a conveniently platform-serving idea Now the uncomfortable one. “Token capital” is a good frame. It’s also an idea that is *spectacularly* convenient for a company whose business is renting you tokens. Walk the logic. If every firm in every sector becomes convinced it must build proprietary “token capital” — private evals, private RL environments, [queryable knowledge bases](https://sublimecoding.com/blog/phoenix-rag-pgvector-arcana), a compounding learning loop — then every firm in every sector becomes a heavier, stickier, longer-term consumer of model inference and cloud infrastructure. Whose cloud? Well. The essay is, among other things, a beautifully argued reason to spend more on the platform that published it. I want to be precise, because the cynical version of this point is as lazy as the doomer version of the last one. The argument can be **simultaneously true and self-serving.** Those aren’t in tension. It is genuinely good advice for a firm to own its learning loop rather than cede all its value to a handful of foundation models — Nadella even makes the macro case himself, warning against “a world where every company across every sector is ceding value to a few models that eat everything they see,” and invoking the way “entire industrial economies were hollowed out by outsourcing.” I think he means it. I also think the prescription that follows from it (“build token capital, on a frontier *ecosystem*”) routes an enormous amount of spend toward the ecosystem he’s selling. Both things are real. The practical takeaway for a founder isn’t “ignore him.” It’s “separate the diagnosis from the prescription, and price the prescription yourself.” The diagnosis — don’t let the model commoditize your knowledge — is sound and free to act on. The prescription — build heavy proprietary AI infrastructure — has a vendor’s thumb on it, and you should adopt only the slice that survives your own cost-benefit math. For a four-person team, that slice is the cheap, durable stuff in critique #1, not a six-figure cloud commitment. There’s an adjacent, larger critique floating around right now about the “loopification” of AI financing — the circular arrangements where model providers, cloud providers, and chipmakers fund and buy from each other in ways that can make demand look more organic than it is. I’m not going to claim Nadella’s essay is *about* that; it isn’t, and stretching it that far would be the kind of overreach I’m criticizing. But it’s worth keeping in your peripheral vision: when the entire supply chain has a stake in convincing you that you must accumulate token capital, treat “you must accumulate token capital” as a claim to verify, not a given. ## The version worth keeping Strip the enterprise scaffolding away and Nadella’s best line stands on its own at any scale: “You can offload a task, or even a job, but you can never offload your learning.” That’s the whole thing. The model is rented and commoditizing by the month. What compounds — what is actually yours — is the judgment your team accumulates about *your* problem, *your* users, *your* codebase, and the discipline of encoding that judgment somewhere the agents can use it. The job shifts from doing to directing and verifying. The moat is compounding judgment, not the model. Everything else in the essay is implementation detail, and most of the implementation detail is sized for a company a thousand times larger than yours. Which is the same conclusion from a different direction: [the schlep is the part agents cannot cross](https://sublimecoding.com/blog/schlep-is-the-moat-agents-cant-cross). ### What a founder or small team can do Monday You don’t need a platform team to start the loop. You need four habits: - **Write the conventions down.** Start a `CLAUDE.md` (or equivalent) today. Every time you correct the agent on the same thing twice, the correction goes in the file. This is your scaled-down “hill-climbing machine,” and it costs nothing. - **Make your review gate the eval.** Stop reaching for the next model release. Treat your existing PR review and tests as the measure of whether the AI is actually helping, and pay attention to *which* tasks it gets right unsupervised. Route work accordingly. - **Capture judgment, not just code.** Spend an hour a week turning what’s in your senior people’s heads — why this choice, what “done” means, which mistakes recur — into text. That’s your queryable institutional memory, no vector database required. - **Close the loop by measuring the outcome.** Pick the one number a change was supposed to move, and check it. A learning loop you never measure is just a faster way to ship things you can’t evaluate. And one thing to do with your *team*, not your tooling: say out loud that the work is moving from execution to judgment, and that you’re going to help everyone get to the judgment side of that line. Nadella calls the end state a “stable equilibrium.” Maybe — but equilibrium is something a firm builds deliberately, by deciding who it carries through the transition. That’s a leadership choice, not an emergent property of the technology. Make it on purpose. Nadella’s right about the map. Just remember it was drawn from an altitude where you can’t see the four-person teams who feel the weather first. ### Postscript — July 23, 2026 Five weeks after this essay, Nadella published [a follow-up on X](https://x.com/satyanadella/status/2080329851127669104) that makes the routing half of this argument explicit, and first-party. Microsoft is now sending traffic across GitHub Copilot, Excel, and Outlook to its own MAI models wherever they match or beat the frontier alternatives, keeping OpenAI and Anthropic models in the orchestration for the frontier work. And he names the design goal outright: your evals “should continue to hill climb even when any given model has been removed” — with the harness, memory, context, and skills deliberately externalized outside the model. That is habit 2 above, stated as strategy by the largest software company on earth. The small-team version still needs none of the enterprise machinery: the conventions file is your externalized harness, the test suite is your eval, and a one-page task-to-model table is your router. The one thing worth adding since June is the motive inversion. For Microsoft, model independence is offense — route the traffic, keep the margin. For you it’s defense: when a model you depend on gets deprecated, repriced, or quietly degraded, [everything you encoded outside it](https://sublimecoding.com/blog/ai-memory-outlives-any-agent) is what keeps compounding. --- ## Agents Don't Need Prompts. They Need Managers. URL: https://sublimecoding.com/blog/agents-dont-need-prompts-they-need-managers Published: 2026-09-08 Tags: AI, agents, founders, AI startups > **TL;DR:** Claude sessions stop before the work is done, and the closing message often reports the task finished when it isn’t. That’s real — I run agents daily and I’ve watched both halves of it happen. But “the model is lazy” is the wrong diagnosis. A human employee arrives wrapped in a management system nobody had to build: a queue, a standup, a manager who notices when they go quiet, a peer who catches the miss. An agent arrives with none of it, and you become all of it, by hand, one agent at a time. Meanwhile two things are true together: companies cut headcount for agents and then [regretted it](https://www.hrdive.com/news/leaders-who-laid-off-workers-due-to-ai-regretted-it/746643/) at scale, and the vendor has shipped limits that cap exactly the behavior operators want — Anthropic shipped weekly rate limits in August 2025 [aimed explicitly at people running Claude Code “continuously in the background, 24/7.”](https://techcrunch.com/2025/07/28/anthropic-unveils-new-rate-limits-to-curb-claude-code-power-users/) The gap between those two facts is where the frustration lives. ## Why does an agent stop before the work is done? Because it arrived without the management system that keeps a person from doing the same thing. Run agents on real work long enough and you converge on a pattern. A session ends before the task is actually finished, and the last message says some version of “this still isn’t complete” — noting the gap instead of closing it. Told to be thorough, it cuts a corner anyway. ([The comic version of what that feels like day to day](https://sublimecoding.com/blog/programming-sucks-now-with-agents) is its own post.) Pushed on the corner, it admits to skipping it, plainly, and then explains what would fix it — which raises the obvious question: why explain it instead of doing it? I’ve watched all of that happen on this repo and others, and the worst version isn’t the early stop. It’s the agent reporting completion on work it didn’t do. That’s a different, more serious failure than laziness. What I want to argue first is that “the model is lazy” is a correct observation aimed at the wrong layer, and the correct read costs a lot more money than the wrong one. ## What a person arrives wrapped in Hire a mid-level engineer and you get a person. You also get, for free, an apparatus around that person that took your company years to build and that nobody thinks of as a feature. There’s a queue that tells them what’s next when they finish, so the end of a task isn’t the end of the work. There’s a standup where somebody would notice they’ve been stuck since Tuesday. There’s a manager whose actual job is detecting when work has stalled and applying a nudge. There’s a code review that catches the shortcut. There’s an escalation path for the moment the task turns out to be underspecified — which is most tasks. There’s memory: they remember the incident in March, they remember that you hate that pattern, they remember they said they’d circle back. Not one of those is the employee. All of them are the system around the employee. Take a genuinely excellent engineer and remove all of it — no queue, no manager, no review, no memory of yesterday — and you get someone who does good work in bursts, stops when the current thing looks done, and has no mechanism to know they stopped short. That is precisely the thing people are describing when they say an agent can’t work independently. The independence they’re missing was never a property of the worker. It was a property of the org. ## The reversal wave is that bill coming due Orgvue’s 2025 survey of more than a thousand business leaders found that about four in ten had laid people off as a result of deploying AI; [55% of those now say they made the wrong call](https://www.techrepublic.com/article/news-leaders-regret-ai-driven-layoffs/). Orgvue’s CEO Oliver Shaw put it about as bluntly as a vendor ever puts anything: “[dismissing employees without a clear plan for workforce transformation is reckless](https://www.hrdive.com/news/leaders-who-laid-off-workers-due-to-ai-regretted-it/746643/).” Robert Half found that [more than three in ten](https://www.forbes.com/sites/rachelwells/2026/07/26/ai-layoffs-are-backfiring-did-employers-bet-too-much-on-the-ai-boom/) US hiring managers who eliminated a position after implementing AI later added it back, and when [Gartner asked over 320 customer service leaders in October 2025](https://www.forbes.com/sites/rachelwells/2026/07/26/ai-layoffs-are-backfiring-did-employers-bet-too-much-on-the-ai-boom/) whether they’d actually cut human support staffing because of AI, only about a fifth had — most held headcount steady and served more customers with it, roughly the same shape I found when I [checked the McKinsey numbers](https://sublimecoding.com/blog/nobody-got-replaced-agents-got-added) that had 25,000 people supposedly replaced by AI agents. What the reversals have in common isn’t that the models underperformed. It’s that the buyer removed the humans doing supervision, quality control, and exception handling, kept the part the machine could do, and found out the removed part was load-bearing. I’ve run the capacity math behind that in more detail in [“why AI won’t shrink your team.”](https://sublimecoding.com/blog/ai-wont-shrink-your-team) ## And the vendor would like your agent to stop One paragraph on this, because it’s not the whole story. Anthropic introduced weekly rate limits effective August 28, 2025, and the stated reason was subscribers running Claude Code *“continuously in the background, 24/7”* — the company said it would affect under 5% of subscribers. Read that against the frustration above: the exact behavior operators want is the exact behavior the vendor named and capped. You don’t have to invent a motive to notice that — draw your own conclusion about why. Netflix asks “are you still watching?” after [three episodes and ninety minutes](https://help.netflix.com/en/node/114059) of uninterrupted play, and Netflix’s own help page gives two reasons, both framed as the viewer’s benefit: saving your place, and not burning your data on a show nobody’s watching. The bandwidth on the other end is a real cost line too, and Netflix doesn’t need to say so. Inference is a real cost line too, and right now it’s still subsidized — I [put numbers on that when I priced the plan tiers](https://sublimecoding.com/blog/ai-coding-agent-costs-real-numbers) — which means every stopped agent session is margin. That doesn’t make the model lazy by design. It does mean nobody upstream of you is strongly motivated to solve continuous unattended operation on your behalf, so stop waiting for them to. ## Build the layer yourself Here’s the part that changes outcomes. Everything a manager does for a person has a mechanical equivalent you can build, and none of the equivalents are exotic. **Put the state on disk, not in the conversation.** The model’s working memory ends when the turn ends. A file doesn’t. A task list the agent reads at the start of a session and updates as it goes is the single highest-leverage thing in this list, because it converts “remember what we were doing” from a context problem into a filesystem read. I run this as a literal `tasks/todo.md` in this repo — the agent checks items off as it finishes them, and the next session starts by reading the file instead of reconstructing intent from a transcript. This is also why a stalled session isn’t really about the size of the context window: you don’t want one enormous run, you want many small ones that share a checkpoint. Anything the agent knew mid-session that isn’t written down is knowledge you’re about to lose the moment the session ends, regardless of why it ended. I wrote a longer version of this pattern — treating a `CLAUDE.md` and its adjacent files as onboarding documentation for a new hire who has no memory between shifts — in [the case for treating your AI config like an onboarding doc](https://sublimecoding.com/blog/claude-md-onboarding-doc). **Make the important things reflexes instead of requests.** Anything you have to ask for, the model can skip under pressure. Anything wired into the harness, it can’t, because it isn’t the model doing it — it’s a program that runs regardless of what the model decided. On this site, a hook runs `mix format` after every file edit, so a formatting break can’t reach a commit no matter how tired or rushed the agent got mid-task. A second hook denies writes to `.env` outright, full stop, no negotiation with the model about whether this edit is the exception. A third validates a content JSON file after every edit and blocks the write if the file no longer parses — because a plausible-looking edit that breaks JSON is exactly the kind of thing a model will confidently make and not notice. I [wrote up the four I run everywhere](https://sublimecoding.com/blog/four-claude-code-hooks); the point isn’t those four specifically, it’s the category. An instruction in a prompt is a request. A hook is a reflex, and reflexes don’t degrade when the model is nine hours into a long session. **Gate on output, not on effort.** “Be thorough” is not a specification and there’s no way to verify it happened after the fact. A preflight check that returns READY or FIX REQUIRED is a specification. A test suite that must pass before work is called done is a specification. The difference matters because a model optimizing to satisfy an instruction will satisfy the literal instruction — if the instruction is “be thorough,” the model can produce something that reads as thorough without being complete, and neither of you will notice until it ships. If your only mechanism for getting careful work is asking for careful work in the prompt, you have no mechanism; you have a hope. **Cut the work small enough that stopping is cheap.** Most of the pain in a long agent session isn’t the stop itself, it’s that the unit of work was too big to verify in pieces, so a failure surfaces at the very end instead of at the seam where it happened. When a task is scoped to something with a checkable output — a function that either passes its test or doesn’t, a file that either parses or doesn’t — an agent quitting early is a visible, recoverable event instead of a silent one buried three files deep in a diff nobody read closely. **Notice when the “nudge” you keep giving is actually a missing input.** This is the one I’d want a frustrated operator to sit with longest. When you find yourself typing “continue” for the fourth time in a session, that’s usually not the model being lazy — it’s the model reaching the edge of what you actually specified and stopping at a boundary you drew without meaning to. The fix lives upstream, in the brief, not downstream, in the nudge. Write the task list once, specifically, and the agent works through it faster than you can type the next prompt. That’s the trade, and it’s a good one, but it only pays off if you do the upfront work instead of narrating the gap after the fact. The reason this feels like a lot of work is that it *is* the work. It’s the same work your company already did for humans, over years, mostly without anyone framing it as work at all. Nobody skips it and gets away with it. What people are calling a model problem is a missing org chart. ## What no harness fixes If I stopped there this would be vendor defense, so: some of this really is the model’s fault, and no hook I’ve described touches it. A model reporting completion on work it didn’t do is not a supervision gap you can gate around, because the harness has no way to know what the task was supposed to mean in the first place — it can check that a response was produced, not that the input was actually read. In my own long sessions, quality seems to erode well before the context window is anywhere near full; I don’t have a benchmark for that — it’s a pattern across a lot of sessions, not a measured curve. I’ve written before about [the specific bugs this produces](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong) and about how [an agent has no stake in whether the code is good](https://sublimecoding.com/blog/why-ai-agents-write-unmaintainable-code) — both remain true and neither is going away this year. Anthropic’s own Opus 5 launch materials claim ground on exactly this failure mode. Their published example: given a real bug in a popular open-source package manager, Opus 5 found the root cause and fixed an edge case the community’s own patch had missed, while “[a competing model fixed only the surface symptom (not the underlying cause), then reported the bug resolved.](https://www.anthropic.com/news/claude-opus-5)” That’s the same shape of failure this post is describing, named by a vendor, in its own launch copy, as the failure its competitors ship. Whether it’s actually fixed at the rate the marketing implies is a separate question I can’t settle from a launch page — but either way, the model layer moving is not a reason to skip building the management layer. It’s a reason to expect the gap to keep moving rather than close. The practical mitigation is unglamorous: stop handing the model the whole history. Give it the recent turns and a summary you control. Ask for the answer *and* the evidence for it, so work the model skipped shows up as a missing citation instead of a confident sentence. There is published support for the instinct — Liu et al. found model performance degrades markedly depending on [where in a long context the relevant information sits](https://arxiv.org/abs/2307.03172) — so assume anything that landed in the middle of a long context wasn’t really read, and structure the task so that assumption is cheap to be wrong about. And a caution I’d apply to my own argument here. Building this layer works, and it does not produce an unattended agent. It produces one that needs fewer, better-placed inputs from you. If a plan depends on the layer eventually reaching zero human input, that plan hasn’t been demonstrated by anyone, including me. ## What to price before you cut a role for an agent Write down what the person you’re removing actually supplies, separately from the task they perform. The queue, the noticing, the exception handling, the memory of why the last attempt failed. If the answer is “the agent does the task,” you’ve priced one column of a two-column problem, and the Orgvue number above says you’ll find the second column later, at a worse price, once it’s already gone. Say who the manager is. Not metaphorically — name them. Every agent doing real work needs a human whose job includes noticing when it stopped — and [knowing which calls that person should be making themselves](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) is a different skill from building the scaffolding, and if that human is also doing their own full-time job, you’ve just found your actual capacity limit. I made this argument in more detail in [“Nobody Got Replaced, Agents Got Added”](https://sublimecoding.com/blog/nobody-got-replaced-agents-got-added): “The supervision doesn’t disappear. It changes shape and moves up a level.” That line is doing more work than it looks like — it’s the whole argument, compressed to one sentence. Then build one piece of the layer before you add the next agent. One hook, one task file, one gate that blocks a bad output instead of asking politely for a good one. That ratio improves from the supervision side far more reliably than from adding more agents — more agents in one conversation tend to [agree their way into the wrong product](https://sublimecoding.com/blog/multi-agent-teams-agree-into-garbage) — which is roughly the shape of the argument I made about [what token economics actually do to the size of a firm](https://sublimecoding.com/blog/nadella-token-capital-small-teams). The failure mode is real. The diagnosis was wrong. What looks like an unmotivated model is an unmanaged one, and you’re the only one in a position to fix that — the vendor has told you, in its own launch copy and its own rate-limit announcement, that it isn’t going to do it for you. You already cut headcount on an agent thesis, and the quality is drifting in a way nobody on the team can quite name yet — that’s the conversation I have with founders most often right now, more often than the sizing conversation that usually starts it. [Let’s talk](https://sublimecoding.com/consulting). --- ## Nobody Got Replaced. Agents Got Added. URL: https://sublimecoding.com/blog/nobody-got-replaced-agents-got-added Published: 2026-08-07 Tags: founders, AI, agents, AI startups > **TL;DR:** The claim making the rounds — that McKinsey replaced 25,000 of its 60,000 employees with AI agents — is not what happened. On [the HBR IdeaCast in January](https://hbr.org/podcast/2026/01/where-mckinsey-and-consulting-go-from-here), Bob Sternfels put the firm at 60,000 total: about 40,000 humans *plus* roughly 20,000 agents, a number he revised upward to about 25,000 at CES days later. The agents were added on top. The human headcount did fall — from around 45,000 at the end of 2023 to about 40,000 — but that took two years and McKinsey attributes it to attrition and performance management. Meanwhile the buried number is the one founders should care about: client-facing roles are *up* about 25%. This isn’t a story about replacement. It’s a story about reallocation, and the question it should provoke in your next planning meeting isn’t “how many people can I cut” — it’s “how many agents can one of my people actually supervise before the quality drops.” ## What was actually said The viral version is clean and wrong: 60,000 employees, 25,000 replaced by AI. It’s a good number. It fits in a headline, it confirms something people already half-believe, and it takes about four minutes to check. Here’s the actual quote, from Sternfels on the IdeaCast on January 6: > “I now update this almost every month, but my latest answer to you would be 60,000, but it’s 40,000 humans and 20,000 agents.” Sixty thousand is humans *plus* agents. It’s a headcount figure that includes software. The 40,000 humans didn’t get reduced to arrive at that number — the 20,000 agents got stacked on top of them to produce it. A day later at CES, Sternfels put the agent count closer to 25,000, and a McKinsey spokesperson confirmed the higher figure as the current one. Eighteen months before that, the firm was running a few thousand. So where does a real reduction show up? It does exist, and it’s worth stating precisely because the honest version is still significant: McKinsey went from roughly 45,000 people at the end of 2023 to about 40,000 — [a drop of more than 10% over about eighteen months, the largest in the firm’s history](https://www.irishtimes.com/business/2025/05/28/mckinsey-sheds-10-of-staff-in-2-year-profitability-drive/). The firm [rejects the layoff framing](https://www.peoplematters.in/news/performance-management/mckinsey-confirms-over-5000-layoffs-in-massive-reset-of-global-workforce-45731), attributing the decline to natural attrition plus its normal performance-management process, alongside a 2023 restructuring that eliminated around 1,400 back-office roles. That’s about 5,000 people over two years, during a consulting downturn that followed five years of near two-thirds headcount growth. Real, consequential, and roughly a fifth the size of the number that went viral — and driven substantially by a market cycle, not a model swap. ## The number they cut out is the one that matters Here’s what bothers me about the popular retelling, and it’s not just that the figure was wrong. It’s that the correction usually stops at “actually, nobody got replaced,” which leaves the most useful finding on the floor. At CES, Sternfels described the shift as “25 squared.” [Client-facing consulting roles up about 25%. Non-client-facing roles down about 25%. And output from that shrinking non-client-facing group up about 10%](https://finance.yahoo.com/news/mckinseys-ceo-breaks-down-ai-100301404.html). Read that again with a founder’s eyes. The part that got amplified was the shrinking group. The part that got dropped was that *the client-facing side grew by a quarter*. This is not a firm getting smaller. It’s a firm moving people from the back office to the front and using agents to hold the back-office output up while it does. And then the sentence that should actually rattle anyone building a company, which Sternfels said in the same breath: > “Our model has always been synonymous that growth only occurs with total head count growth. Now it’s actually splitting.” The head of a firm whose entire business model was billable humans just said the link between headcount and growth is coming apart at his own company. That’s the finding. It survived the fact-check, it’s on the record, and it’s more disruptive than the fake version — because the fake version says “fire people,” which is a one-time event, and the real version says “your unit of capacity changed,” which is a permanent structural fact you now have to plan around. The fabricated number told founders to cut. The real number tells them to reallocate — and reallocation is a much harder thing to get right. ## Why the wrong number traveled It’s worth being honest about why a claim like this spreads, because the mechanism will produce the next one too, and you’ll be the target of that one as well. Numbers about AI displacement are not neutral facts moving through the world on their own merit. They’re capital-allocation instruments. Whether they’re true is a secondary property. Consider the incentives. A large company that has committed enormous sums to hardware, data centers, and model access needs that spend to look like it’s working. There’s a shape this takes that I’ve written about before: [AI made tokens cheap and it’s making hardware costly](https://sublimecoding.com/blog/ai-tax-hardware-costs), and the companies deepest into that spend are the ones with the strongest reason to announce that it’s paying off. Announcing a large headcount reduction attributed to AI does several things at once — it signals cost discipline to the market, it justifies the capital expenditure, and it positions the firm as ahead. Whether the reduction was actually caused by AI or by a demand slump that would have happened anyway is not a distinction the press release is built to make. Now add the investor layer. Venture capital is structurally biased toward sweeping change — the entire return model depends on finding the thing that resets an industry, so a claim that an entire labor category has been automated is exactly the kind of story that attracts capital toward the companies telling it. That’s not a conspiracy, it’s just what the incentive gradient looks like. A dramatic claim gets amplified because amplification serves the amplifier. So when a number like this crosses my feed, my procedure is short and it’s mostly one question, asked before anything else: **what does the person saying this need to be true?** That’s the same test I ran on [a vendor’s 96% security benchmark](https://sublimecoding.com/blog/ai-security-benchmark-what-96-percent-means) and on [the way Anthropic’s safety asks were bundled](https://sublimecoding.com/blog/jose-valim-anthropic-incentive-problem), and it’s the test that would have caught this one immediately — because the claim was being repeated most enthusiastically by people selling AI transformation to executives. After that question, the rest is mechanical: find the primary source, not the article about the source. Read the actual transcript or watch the actual talk. Check whether the eye-catching number is a total, a delta, or a projection, because those get swapped constantly. And check the timeline — “replaced 25,000 people” and “grew to 25,000 agents over two years” describe completely different events, and the difference lives entirely in a verb. Four minutes. It’s not investigative journalism. It’s just not taking the headline’s word for it. And it matters, because [the last time everyone repeated a confident AI story without checking the mechanics, the story was Amazon’s, and the details were considerably less flattering than the summary](https://sublimecoding.com/blog/amazon-let-the-ai-drive). ## Addition is the pattern that works Now the part I actually believe, which is the reason the McKinsey numbers are interesting rather than just misreported. The addition pattern is the correct one. Not because it’s gentler, but because it’s the one that compounds. The company that trains its existing staff to direct a growing number of agents ends up with people who can each carry vastly more scope. The company that cuts staff and hands the remaining work to agents ends up with fewer people, each of whom is now responsible for reviewing more output than they can actually review, and no one left with the context to catch the things that go wrong. One of these is a capability build. The other is a cost-cutting exercise wearing a technology costume, and I think it’s short-sighted in a way that will be expensive to unwind. The teams that cut deepest will be hiring back in eighteen months, at worse terms, for people who now have to reconstruct institutional knowledge that walked out the door. There’s a second-order problem underneath this that I keep coming back to: agent-heavy development increases your blast radius and your velocity simultaneously. You can do more things, faster, in more places. That means *more* needs reviewing, not less — and the reviewing is the part that requires judgment, context, and someone who will be there when it breaks. [I’ve argued that we’re about to stop producing senior engineers](https://sublimecoding.com/blog/stop-making-senior-engineers) precisely because the pipeline that made them ran through the work agents now do. Cutting the humans who do the reviewing, to pay for the agents who generate the things needing review, is the specific move that breaks this. ## The question to actually put in the board deck So here’s the replacement for “how many people can AI let us cut.” **What’s your human-to-agent supervision ratio, and what’s your evidence for it?** I’ll give you my numbers, from running this daily, so you have something concrete to argue with. For menial work — organizing files, summarizing notes, mechanical scans, the grunt tier — I’ll run five to ten agents at once without much strain. The work is checkable at a glance and the failure modes are boring. For actual engineering, writing and changing code that has to work, it’s **five to six at a time**. Sometimes up to ten, depending on how good the test coverage is on what they’re touching — strong tests raise the ceiling because the tests do part of the supervision for me. Push past that and the quality doesn’t degrade gracefully, it degrades in a specific way: I stop reading the diffs properly and start skimming them, and skimming a diff is functionally the same as not reviewing it. The agents don’t get worse. My attention does. It’s a spectrum, and it moves. Some days each agent needs more babysitting and the number drops. Some tasks are well-fenced enough that it climbs. Anyone who gives you a fixed universal ratio is selling something. For an outside data point: Cherny’s setup at Anthropic runs [roughly five terminal sessions against separate worktrees plus five to ten cloud sessions](https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny), with sub-agents fanning out underneath. Different tooling, different scale of delegation, but the number of things one experienced person actively steers lands in a strikingly similar place. Now hold that against 25,000 agents and 40,000 humans and do the arithmetic. That ratio is well under one agent per person, which is exactly why McKinsey’s version is coherent — Sternfels has framed the near-term goal as every employee being enabled by at least one agent. It’s an augmentation ratio, not a replacement ratio. If your plan involves anything like five or ten agents per remaining human, you are not proposing the McKinsey strategy. You’re proposing something nobody has demonstrated, and the binding constraint won’t be model capability. It’ll be how many diffs a tired person will actually read on a Thursday afternoon. That constraint is real, and it’s the one worth designing around. You can push it — better tests, tighter task scoping, agents that review other agents, structures inside your agent fleet that mirror the org structures companies already use for humans. I think that’s where this goes, and I think one person eventually supervises far more than six. But every one of those structures is itself something a human has to build, maintain, and debug. The supervision doesn’t disappear. It changes shape and moves up a level. ## The thing that doesn’t scale One more, and it’s the piece I think gets left out of every one of these headcount conversations. An agent has no motivation. It doesn’t need to feed anybody. It isn’t trying to make rent, or get the promotion, or avoid being the person who broke production in front of the whole team. It has no stake in whether the company exists next year. It does the work it was handed because it was told to, and that’s the entire depth of it. Every reason anything gets done in your company is a human reason. Someone cares about being good at this. Someone doesn’t want to let their team down. Someone has ambitions that require this project to succeed. That layer is not a soft benefit sitting on top of the real work — it’s the thing that generates the direction the agents then execute. You can add 25,000 agents and get more throughput. You cannot add 25,000 agents and get more *intent*, and intent is the scarcer input. That’s a bigger argument than fits at the end of this post, and I’ll make it properly on its own. But it belongs in the frame here, because the headcount question is usually posed as if people and agents were the same kind of thing in different quantities. They aren’t. One of them supplies the reason. ## Before your next hire decision Three things, concretely. Check the number that’s driving the decision. Open the primary source. Ask what the person saying it needs to be true. If a board deck’s thesis rests on a statistic somebody screenshotted, that’s not a thesis. State your supervision ratio explicitly and defend it with something. Not a vendor’s claim — your own observation of when your people stop reading diffs carefully. If nobody on your team can tell you that number, you don’t yet know your actual capacity, and you certainly shouldn’t be sizing headcount against it. And ask Sternfels’s question rather than the fake one. Not “how many people can we replace,” but “which roles move toward the customer, and what holds up the work they’re moving away from.” That’s the split he was describing, and it’s a reallocation problem — which is harder than a cutting problem, and considerably more likely to still be working in two years. [I’ve made a version of this argument before about what AI actually does to the size of the firm](https://sublimecoding.com/blog/nadella-token-capital-small-teams), and everything since has pushed me further in the same direction. If you’re a founder trying to size a team against agent-heavy delivery — and trying to work out which of the numbers landing in your inbox are load-bearing and which are marketing — that’s exactly the kind of question I help founders get right before it’s baked into a plan. [Let’s talk](https://sublimecoding.com/consulting). --- ## AI Made Tokens Cheap. It's Making Hardware Costly. URL: https://sublimecoding.com/blog/ai-tax-hardware-costs Published: 2026-06-26 Tags: AI, AI startups, founders > **TL;DR:** On June 25, 2026, Apple raised prices across the Mac, iPad, HomePod, and Apple TV lines — the iPad Air jumped $150 to $749, the Apple TV $70 to $199 — and pinned it on a memory-chip shortage Tim Cook called a “hundred-year flood,” driven by AI data centers buying up the world’s DRAM. The reflexive read is “the AI tax has arrived; everything’s getting more expensive.” That’s half the ledger. The cost of *intelligence* — the price of a token, of a unit of inference — is still falling fast. What’s repricing is the *physical substrate* that runs it. Those two lines move in opposite directions, and if you’re building anything on AI, the expensive mistake is modeling only one of them. My read: this isn’t a temporary cycle. It’s a structural reallocation that holds as long as data centers keep outbidding the rest of us for the same silicon. ## Apple blinked, and named the reason Apple does not raise prices on a shipping lineup mid-cycle. It announces a new product at a new price, lets the old one fall away, and otherwise holds the line for a year. On June 25 it broke that habit across nearly the entire non-iPhone catalog at once. The iPad Air went from $599 to $749. The iPad Pro added $200 to reach $1,199. The entry-level MacBook Neo climbed to $699, the MacBook Air to $1,299, the MacBook Pro to $1,999. The Apple TV 4K jumped from $129 to $199; the HomePod from $299 to $349. The iPhone was the conspicuous exception. What’s unusual isn’t the increase — it’s that Apple said *why*, out loud. The company blamed rising costs for memory and storage chips, and Tim Cook, who has run Apple’s supply chain for over two decades, [called the shortage a “hundred-year flood,”](https://9to5mac.com/2026/06/25/apple-price-increases-mac-ipad-more/) adding that he’d never seen anything like it in over forty years. The market took it as more than an Apple story: [Apple’s stock fell roughly 5% and Dell’s more than 8%](https://www.cnbc.com/2026/06/25/apple-macbook-ipad-price-hike-memory.html) as investors priced in memory pressure across the whole consumer-hardware supply chain. The [full price table is worth a look](https://www.macrumors.com/2026/06/25/apple-just-increased-prices/) if you want the damage line by line. The cause Cook named is the part that matters here. The DRAM and high-bandwidth memory going into AI data centers is the same memory that goes into a laptop. When demand from one buyer goes vertical, everyone else in the line pays the spread. ## “The AI tax” is half the story The easy take wrote itself within the hour: *this is the AI tax, and it’s coming for everything.* I’d push back on that the same way I push back on its mirror image. I’ve spent a lot of words on this site arguing the clean version of the AI-cost story is usually wrong — that [“AI will shrink your team”](https://sublimecoding.com/blog/ai-wont-shrink-your-team) is too tidy to be true, and so is “AI will shrink your bill.” The reality is lumpier, and it’s lumpy here too. Because at the same moment Apple is raising hardware prices, the price of *intelligence itself* is doing the opposite. Per-token inference cost has been on a steep deflation curve for three years and shows no sign of flattening. I made the broader version of this point responding to Satya Nadella’s [“token capital” essay](https://sublimecoding.com/blog/nadella-token-capital-small-teams): the AI capability a firm can buy gets cheaper to accumulate every quarter. So why is the metal getting more expensive in the very same quarter? Because it’s the same demand doing both. The data-center buildout that drives per-token cost *down* — through scale, competition, and relentless efficiency work — is the same buildout bidding the price of every advanced memory die *up*. Cheaper intelligence and pricier hardware aren’t a contradiction. They’re two readings off the same gauge. Apple’s laptop and your inference server are now standing in the same checkout line as a hyperscaler’s GPU cluster, and the hyperscaler has a much bigger wallet. ## Two lines on the same invoice If you ship software on AI, this resolves into something concrete: your costs split into two trends moving in opposite directions. - **The intelligence line** — API calls, inference, tokens — trends *down*. Competition and efficiency keep cutting it, and that’s unlikely to reverse. - **The substrate line** — the GPUs you rent, the memory in the box, the dev machines your team runs on a refresh cycle, and now even the laptops — trends *up*. Same root cause, opposite slopes. The naive “AI makes everything cheaper” extrapolation watches only the first line and budgets as if the second doesn’t exist. For most software teams the token line still dominates and still falls, so on net you may genuinely come out ahead — that’s the good news, and it’s real. But the instant your workload leans on owned or rented hardware — local inference, fine-tuning runs, a fleet of machines on a three-year refresh — the second line starts to bite, and it’s the line nobody put in the spreadsheet. This is just [proving the return](https://sublimecoding.com/blog/prove-the-return) applied to a cost base that moves under you. You can’t evaluate whether an AI investment pays off against last year’s hardware prices. Model the return against the cost you’ll actually face — both lines — or you’re proving the wrong number. ## Temporary cycle, or the new normal? So which is it. Memory has always been cyclical; DRAM booms and busts are a decades-old pattern, and the reflexive analyst take — *this too shall pass* — is half right. The amplitude is cyclical. The floor moved. The demand that reset the floor is training and serving frontier models, and that is not a fad quarter. It compounds, and it has the deepest pockets in the economy behind it. As long as a memory die earns more sitting in a data center than sitting in a HomePod, the data center wins the allocation and consumer — and small-business — hardware pays the difference. Prices won’t snap back to 2025 levels, because the demand curve didn’t briefly spike. It shifted. Advanced silicon quietly became a contested strategic resource somewhere around the time it started running the economy’s intelligence, and contested resources don’t get cheaper because you’d like them to. So: not a one-time tax, and not a passing cycle. A reallocation. The cyclical swings will still happen on top of it, but the baseline you’re pricing against is higher now and likely to stay there. The question was never whether AI would change how we live and build. It’s which side of the invoice you’re standing on when the bill arrives. If you’re buying intelligence, it keeps getting cheaper — enjoy it while it lasts. If you’re buying the hardware that runs it, get used to the new number. Apple just did the rest of us the favor of printing it on a price tag. The obvious dodge is to stop buying the Mac. I tried it — [Omarchy on an M1 via Asahi Linux](https://sublimecoding.com/blog/omarchy-asahi-m1-macbook) — and the tax doesn’t disappear so much as change currency. --- ## When to Trust an Agent and When to Step In URL: https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in Published: 2025-12-22 Tags: AI, agents, productivity, engineering **The hardest part of working with agentic AI in 2026 isn't getting the agent to do the work. It's knowing when to override it.** An agent that produces useful code 90% of the time and confidently broken code 10% of the time is not a 90% solution. It's a system you have to be paying attention to all of the time, because the 10% of broken output looks indistinguishable from the 90% of correct output if you skim. Trust calibration is the actual engineering skill of working with agentic AI, and most teams don't yet have a framework for it. What follows is the framework I use, in two parts. First: a four-level autonomy ladder for deciding how much trust to extend to an agent on any given task. Second: the five signals that an agent is currently going off the rails, even when its output looks fine. ## The autonomy ladder, at a glance Four levels of AI agent supervision, set by the consequences of the agent being wrong: - **Level 1 — Read-only.** Agent observes but changes nothing. Code analysis, summarization, search. Run autonomously all day. - **Level 2 — Bounded write.** Small, clearly-scoped writes — tests for an existing function, a single-file refactor. Review the diff before merging, but don't read every line. - **Level 3 — State-changing.** Payments, auth, user data, system state. Agent can draft, but every line gets human review before it lands. - **Level 4 — Public-facing or irreversible.** Production schema migrations without rollback, customer-facing email, public posts. Do not delegate. The five failure signals that demand human takeover — confidence rising as ground truth gets thinner, plausibility without verification, scope creep mid-task, the agent contradicting itself across messages, and silent retries — are detailed below. ## The four-level autonomy ladder Not all tasks are equal. The same agent can be entirely trustworthy on one task and dangerous on another. The level of supervision should be set by the consequences of the agent being wrong. ### Level 1 — Read-only, always trust Tasks where the agent observes but doesn't change anything. Code analysis, documentation generation from existing code, summarization, search. The worst-case outcome of the agent being wrong is that I get bad information that I have to discard. I let agents work autonomously at this level all day. The downside is bounded. ### Level 2 — Bounded write, mostly trust, verify Tasks where the agent writes code in a clearly-scoped area. Adding tests for an existing function. Implementing a small utility from a clear spec. Refactoring a single file. The blast radius is small, the work is reviewable, and the agent has a high probability of getting it right. I review the diff before merging, but I don't read every line carefully. I'm looking for obvious smell — duplicate logic, weird naming, missed edge cases. If the diff looks clean and the tests pass, I merge. The downside if I miss something is one bad commit that's easy to revert. ### Level 3 — Real-money, auth, or state-changing, verify line by line Tasks that touch payments, authorization, user data, or system state in ways that matter. The agent can *draft* these, but every line has to be reviewed by a human before it lands. This is the level where most teams lose discipline first. The agent produces a plausible-looking auth migration, the diff isn't huge, the tests pass — but the migration is silently introducing a privilege escalation. I've seen this happen. I've nearly let it happen, which I'll talk about in a moment. The discipline at this level: read every line, run the change against your own threat model, ask the agent why it made each non-obvious choice. Treat the agent's output as a junior engineer's work that needs senior review before merge. ### Level 4 — Public-facing or irreversible, do not delegate Tasks where the cost of getting it wrong is unrecoverable. Schema migrations on production data without rollback. Sending email to your customer base. Posting to a social media account. Public-facing legal text. Press statements. Agents do not produce these autonomously. They can *draft* them — which can be useful — but the work of actually committing to the output is human-only. The asymmetry is too sharp; even a 99% reliability rate produces an unacceptable error rate over many actions. ## How do you know when an AI agent is going off the rails? Even within the right autonomy level, individual sessions can drift. The signals that something is off, in approximate order of how often I see them: ### 1. Confident answers without specifics "This will work because of how the framework handles state." Without naming the function, the file, or the documented behavior. The agent is filling in plausible reasoning rather than checking. Push back: "Show me the line in the codebase that demonstrates this." If the agent can't, the answer is suspect. About half the time the agent then says "actually, on closer inspection..." and revises. The other half it doubles down on a wrong claim, which tells you the entire reasoning chain is hallucinated. ### 2. Multiple files changed for "one fix" You ask for a fix to a single bug. The diff comes back touching seven files. Sometimes this is correct — the bug genuinely was scattered. More often, the agent has decided the codebase needs "consistency" or "improvement" while it was in there. The discipline: ask why each file was changed. If the answer for any file is anything other than "this was necessary for the fix," revert that file's changes. Scope drift in agent diffs accumulates fast. ### 3. "Cleaning up" code unrelated to the task A subtype of the above. The agent removes a comment it didn't understand. Renames a variable. Reformats a function it thought was ugly. None of these are explicit instructions. All of them produce noise in the diff. This is a hard category to police because the changes look harmless one at a time. The cumulative effect over a quarter is a codebase whose history is incomprehensible because every fix touches twenty unrelated lines. ### 4. Confidence that contradicts evidence Tests are failing. The agent says "the implementation is correct, the tests must be wrong." This is occasionally true. It is usually wrong. The signal is the agent privileging its own reasoning over the failing test. The fix: never let the agent dismiss a failing test without proof. "Show me which assertion in the test is incorrect and why" is the right pushback. Most of the time the agent then realizes the implementation is wrong. ### 5. Speed too high for the complexity This one is the hardest to articulate but the most reliable in retrospect. A complex problem is solved in twelve seconds with one paragraph of explanation. Be suspicious. Real engineering problems usually have layers; an instant answer often skips them. The discipline: when the answer comes back faster than seems reasonable, ask the agent to explicitly enumerate three alternatives and argue for the chosen one. The "argue" step surfaces whether the agent has actually thought about the problem or pattern-matched to a familiar shape. ## A concrete example: the auth refactor Earlier this year I asked an agent to refactor a small piece of auth code. The original function had grown to 200 lines and could be reasonably split into four. Routine work, level-3 task by my own framework — auth is in the "verify line by line" category. The agent produced a clean four-function refactor in about 90 seconds. The tests passed. The diff was the right size. On a quick read it looked correct. I almost merged it. Two of the five signals fired before I clicked merge. Speed too high (signal 5): the refactor was elegantly factored, which is suspicious for a 200-line function with the gnarly history this one had. And confidence without specifics (signal 1): when I asked the agent why it had moved a particular permission check from one branch to another, the answer was "it's cleaner this way" rather than "the original ordering was incorrect because X." I dug in. The "cleaner" reordering had introduced a small but real privilege escalation: in one specific code path, an authorization check that previously ran *before* a sensitive operation now ran *after*. In the test suite, no test exercised that exact path, so the tests passed. In production, the bug would have allowed certain users to perform an action they shouldn't have been able to. The discipline that caught it was the framework above. Without it, I'd have merged a clean-looking refactor and shipped a real security bug. Multiply by every team using AI for code generation, and you can see why the slop problem is real and the trust calibration is the actual engineering work of 2026. ## The meta-discipline The framing that pulls all of this together: *agents are tools, not teammates*. They don't have stakes. They don't get yelled at when production breaks at 3am. They have no embodied sense of what's risky. They will confidently produce code that's 99% right, and the 1% wrong will sometimes be catastrophic, and they will not know. The job of the engineer working with agents is to supply that missing sense of stakes. The autonomy ladder is how you decide when to engage. The five signals are how you stay engaged once you're in the work. None of this is hard. It's just disciplined. The teams that get this right are using agents at the limit of what's possible without producing slop. The teams that don't are accumulating a debt that becomes obvious only after a major incident. Calibrate trust. Verify the consequential changes. Override when the signals fire. The framework is the discipline. ## How do you roll out AI agent supervision rules to a whole team? Individual discipline isn't enough at team scale. If only one engineer on a four-person team is calibrating agent trust carefully, the other three's slop ends up in the codebase anyway. The patterns above need to be team-level practice. The lightest-weight version of this that's worked for me: a single shared document — call it `AGENTS.md` — that lists the team's autonomy levels, the signals to watch for, and the kinds of work that always require human review. The doc is short, written by the team together, and reviewed quarterly. Pair it with one tactical practice: every PR description includes a single line at the top stating which autonomy level applied to the work. "Level 2 — bounded write, agent-assisted." "Level 3 — auth code, full human review." This makes the trust calibration legible during code review and surfaces drift before it ships. The combination of the doc and the PR-line discipline takes about a week to introduce and a quarter to internalize. The teams that do this end up with a meaningfully better signal-to-noise ratio in their AI-assisted work than the teams that don't. ## Read this next - [**My Daily Agentic AI Workflow**](https://sublimecoding.com/blog/my-daily-agentic-ai-workflow) — The day-to-day mechanics of running multiple agents at once. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — The team-level discipline that complements the individual one above. - [**How I'd Run Security at an AI-Native Company in 2026**](https://sublimecoding.com/blog/running-security-at-an-ai-native-company-2026) — Where agent-trust meets the production security threat model. --- ## How I Triage a New Codebase in 90 Minutes URL: https://sublimecoding.com/blog/triage-a-new-codebase-90-minutes Published: 2025-12-08 Tags: engineering, productivity, engineering leadership, vCISO, staff engineer **A fractional engineering engagement starts with a codebase you've never seen. You have ninety minutes to form a useful POV before the kickoff call.** This is a recurring situation for me as a [fractional engineer](https://sublimecoding.com/consulting). A founder books a discovery call, gives me read-only access to their repo on Tuesday, and the kickoff call is Wednesday morning. Between those two events, I need to know enough about the system to ask intelligent questions, identify the load-bearing risks, and not waste the founder's time with surface-level observations they could have written themselves. The framing that makes this work: *not* "understand the codebase." That takes weeks. Instead — *find the load-bearing risks*. The seven-step triage I run, in order. ## The 90-minute triage, at a glance - **0–10 min — README + architecture docs.** What does the team think the system does? - **10–15 min — Dependency audit.** Major frameworks and how out-of-date everything is. - **15–25 min — git log story.** Who works on what, how big are the commits, what's the recent panic? - **25–35 min — Tests and CI signal.** What's covered, what's skipped, what's flaky. - **35–45 min — The hottest files.** The five files that change most. That's where the load-bearing risk lives. - **45–60 min — Auth and data layer.** The two places a small mistake becomes an incident. - **60–70 min — The most recent incident.** What broke, what fixed it, what didn't. - **70–90 min — Notes consolidation.** The five questions you'll ask on the kickoff call. ## Step 1: README.md and any architecture docs (10 minutes) Start at the front door. The README tells me three things almost immediately: how the team thinks, how recent the project's intentional documentation is, and whether the founders or engineers wrote it. Signals to look for: when was this last meaningfully updated? Does the "how to run locally" section reference real commands or stale ones? Are there architecture diagrams, ADRs, or design docs anywhere in the repo? An empty `docs/` directory and a README that ends with "TODO: write more about this" tells you a great deal about engineering culture before you've read a single line of code. I copy the relevant facts into my own running notes. The first paragraph of those notes is "what the team thinks the system does." ## Step 2: Dependency audit (5 minutes) Open `package.json`, `mix.exs`, `requirements.txt`, `go.mod`, or whatever the equivalent is for the language. Two questions: what major frameworks are in use, and how out of date is everything? The dependency list is a faster summary of the system's architecture than reading the architecture docs. If I see `phoenix_live_view` + `oban` + `ecto`, I know the shape of the app. If I see thirty random utility libraries and no obvious framework, I know there's been turnover or a lack of opinionated leadership. For the freshness check: a dependency that's two majors behind isn't automatically bad, but a codebase where *every* major dependency is two-plus versions behind is a maintenance bomb in the making. Make a note. ## Step 3: The git log story (10 minutes) Run `git log --oneline --all -200`. Skim the last two hundred commit messages. What you're looking for: who's committing, what they're committing, and what the rhythm looks like. A repo where one engineer wrote 90% of the recent commits is a key-person-risk situation. A repo where the commits are mostly "wip" and "fix typo" tells you about the team's commit hygiene. A repo where every PR has a clear, conventional-commit-style message tells you the team has invested in process. The signal that matters most for triage: pattern frequencies. Lots of "revert" commits in the recent past = unstable changes. Lots of "fix" commits referencing one specific module = that module is troubled. Use the patterns to direct what you read next. ## Step 4: Test coverage and CI signal (10 minutes) Find the test directory. Count files. Find the CI configuration. Read it. Run the test suite if you can — does it pass? How long does it take? You're looking for three signals: - **Volume.** Is there one test file or two hundred? - **Quality.** Open three random test files. Do they test behavior or just exercise code? - **CI status.** Is the build green? Has it been red for more than 24 hours? Is there a culture of merging on red? The test suite is the closest thing a codebase has to a self-portrait. A team that's invested in test quality is a team that takes engineering seriously. A team where the tests don't pass on a fresh checkout is a team that has bigger problems than the ones the founder is going to tell you about. ## Step 5: The hottest files (10 minutes) Run a quick analysis to find the most-changed files in the last six months. `git log --pretty=format: --name-only --since="6 months ago" | sort | uniq -c | sort -rg | head -20`. The top of that list is where the action is. Open the top three or four files. Read them. These are the load-bearing parts of the system, by definition — they're where the team is spending their engineering energy. What you're looking for: are these files long, complicated, and full of inline comments saying "TODO: this is a hack"? Or are they crisp, well-factored, and recently refactored? The hot files tell you where the system is fragile and where it's healthy. They're also the files that an incoming engineer (or fractional engagement) will most likely need to touch first. ## Step 6: The auth and data layer (15 minutes) Now the deep dive. Find the authentication code. Find the database schema. Read both carefully. Authentication: how does a user log in? Where are sessions stored? Is there MFA? Are passwords hashed with a current algorithm? Is there an obvious authorization layer beyond authentication? This is where I find the highest-severity bugs in early-stage codebases — usually authorization issues that the team hasn't yet noticed because they haven't been exploited. Data layer: what tables exist, what relationships do they have, are there migrations that suggest the schema has been refactored, are the indexes sensible? The schema is the contract the system runs under. Anything wrong with it is wrong with everything else. This is the longest step in the triage by design. If I'm going to find a deal-breaker risk, it's almost always in this section. ## Step 7: The most recent incident (10 minutes) Ask the founder (or look in the team Slack history if you have access): when was the last production incident? What broke? How was it fixed? The incident report — written or verbal — tells you more about the engineering culture than any single artifact in the repo. A team that has a clear retro doc with five action items, three of which were completed, is a team that learns. A team where "the last incident" is met with "uh, well, last week the database fell over for an hour, I think someone restarted it" is a team that doesn't. This step also surfaces the thing the founder is most worried about. They'll volunteer it once they sense you're asking real questions. ## What should you actually write down after triaging a codebase? Ninety minutes of triage produces a one-page document with the following sections: - **What the team thinks the system does** (one paragraph, from step 1) - **The shape of the stack** (three lines, from step 2) - **Engineering rhythm** (commit frequency, team size, hot spots — from steps 3 and 5) - **Quality signals** (test coverage, CI, code-review hygiene — from step 4) - **The two highest-severity risks I found** (from steps 6 and 7) - **The two questions I'm bringing back to the founder** The two questions are the most important output. They're how you signal to the founder that you've done real work in 90 minutes and have a useful POV. They're also the questions whose answers will reshape everything you do in the engagement. Examples I've actually used: *"Is the lack of audit logging on the admin endpoints intentional?"* Or: *"Looking at the last six months, your messaging service has been the source of three of the four production incidents — what do you and the team think is going on there?"* ## The AI-assisted version The 90-minute number above predates Claude Code. The same triage now takes about half that time with AI assistance. The pattern: I run the seven steps in parallel using a Claude Code session pointed at the repo. Step 1, 2, 3, 4, and 5 are largely automatable — I tell Claude what to look for and it produces structured summaries faster than I can read the raw files. I spend the saved time on steps 6 and 7, which still benefit from human attention. Net result: the same depth of triage in 45 minutes instead of 90, with substantially better notes because the AI captures things I would have skimmed. The disciplined human still drives. The agent accelerates. ## The takeaway The skill being practiced here is not "reading code fast." It's *knowing what to look at first.* Most engineers, on entering an unfamiliar codebase, dive into the part of the system most relevant to their immediate task and form a partial picture. The triage protocol forces you to look at the system in the order that surfaces risks, not in the order that matches your task. Useful for fractional engineers. Useful for new hires in their first day. Useful for anyone who's about to take on technical responsibility for a codebase they didn't write. Run the protocol. Take the notes. The 90 minutes will save you weeks downstream. ## What do you actually say on the kickoff call after a 90-minute triage? One last note. The triage produces a written one-pager, but the founder is going to want to talk through it on the kickoff call. The framing that lands: lead with what you saw that's working, then name the two highest-severity risks, then ask the two questions. This sequence is deliberate. Founders are sensitive about their codebase — many haven't had an outsider look at it in years. Leading with criticism puts them on defense and shrinks the conversation. Leading with what's working buys you the credibility to then talk about what isn't. The two questions you ask are the most important moment of the call. They signal you've done real work, not just listed problems, and they invite the founder into a conversation about priorities rather than a lecture about deficiencies. Done right, the kickoff call ends with the founder saying some version of "let's start with those two things you flagged" — which is exactly the engagement you wanted to land. ## Read this next - [**The 'Smallest Possible Slice' Heuristic**](https://sublimecoding.com/blog/smallest-possible-slice-shipping-complex-features) — The same pragmatic-decomposition mindset applied to feature work instead of code review. - [**Migrating 225K Users from AWS Cognito to Auth0**](https://sublimecoding.com/blog/aws-cognito-to-auth0-migration-without-forcing-logout) — A real example of step 6 (auth-layer scrutiny) playing out in production. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — How the AI-assisted version of triage scales to other engineering tasks. --- ## My Daily Agentic AI Workflow URL: https://sublimecoding.com/blog/my-daily-agentic-ai-workflow Published: 2025-11-24 Tags: AI, agents, AI tools, productivity **I run four to seven agent sessions in parallel through a normal engineering day. Here's what they do, what they don't, and how I keep the work coherent.** The defining shift in engineering work over the past two years isn't that AI writes code faster. It's that you can have multiple agents working on multiple things at the same time, and your job moves from "writing code" to "directing work." This is qualitatively different from autocomplete, copilot-style assistance, or any prior way of using AI in development. What follows is a walkthrough of a typical engineering day for me as of late 2025, running on Claude Code as the primary tool, OpenAI Codex for shell-shaped tasks, and a few custom agents wired into Slack and the command line. The point isn't the tools — pick whichever you like. The point is the patterns. ## The core idea Agents take work off your plate but still need you in the loop. The trap most teams fall into: treating agents as fire-and-forget background workers. The result is generated code that compiles, looks reasonable, and is subtly wrong in ways the agent has no way to detect on its own. The mental model I use: an agent is a fast, talented junior engineer with infinite patience and zero context outside what you give them. Your job is to give them the context, scope the work tightly, and review the output before it goes anywhere production-shaped. ## Walkthrough of a typical day ### Morning: 2 background agents on long tasks Before I sit down at my desk, I usually have two agent sessions running. The pattern: tasks that take a long time, don't need real-time feedback, and have well-defined success criteria. Examples from a recent week: - "Audit our codebase for places we're calling external APIs without retry logic, and produce a markdown report with recommendations." - "Read the last fifty PRs merged to main, identify recurring code-review feedback themes, and write an internal style-guide draft based on them." - "Generate test cases for the authentication module covering all edge cases listed in the threat model document at `docs/auth-threats.md`." I kick these off, walk away, and come back to a draft in 30–60 minutes. Critically, the output is always a *draft*. I read it, I edit it, I push the parts I trust into the codebase. The agent never commits unsupervised. ### Mid-morning: foreground pair-programming with one agent This is the bulk of my actual coding. I open a fresh agent session for the hardest problem of the day and we work it together — me driving, the agent acting as a peer. The interaction is conversational, not delegating. Concretely: I describe the problem, the agent asks clarifying questions, we sketch an approach, I write some code, the agent reviews, I push back on suggestions I don't like, we iterate. By the time the function is committed, both of us have looked at every line. The mistake to avoid in this mode: letting the agent write the code while you watch. That's still autocomplete, just slightly fancier. The point of pair-programming with an agent is that *you* are still doing the engineering — the agent is checking your work, raising things you might miss, and accelerating the parts that don't require taste. ### Afternoon: review-mode agents on PRs By afternoon I've usually written or merged some code, and I have PRs to review (mine and the team's). I run a review-mode agent on each PR before I read it myself. The instruction is consistent: *"Review this diff like an adversarial senior engineer who hates my work. Find bugs, race conditions, security issues, and unclear naming. Don't be polite."* The agent produces a list. I read the list, dismiss the noise (typically 60–70%), and the rest becomes my review comments — credited to me, of course, but with the agent doing the first pass. The leverage here is significant. The agent catches a real bug or smell about 30% of the time. The other 70% is dismissable noise that I'd have generated mentally anyway. Net: I write better PR reviews in less time, and my human reviewers catch things they otherwise wouldn't have. ### End-of-day: ops agents on deploy + summary Late in the day, two more agents come into play. The deploy agent: a custom Slack bot wired to our deployment pipeline. I tell it "deploy main to staging" or "deploy 4f3a2b1 to production behind feature flag `ai_v2`" and it executes the relevant commands, watches the deploy, and reports back. It does not, ever, have permission to do production deploys without a confirmation. But staging deploys, log queries, and rollback prep — yes, autonomously. The summary agent: at end-of-day it reads the day's commits, the day's PR comments, the day's Slack threads in our team channel, and produces a one-paragraph "what happened today" summary. Useful for me; useful for async teammates; surprisingly useful when I come back on Monday to remember what we were working on Friday. ## The three interaction modes Boil all of the above down and there are really three modes I use agents in. Each one has a different signature. - **Delegate.** Long-running task, well-defined output, light supervision. Background mode. The success criterion is whether the deliverable is useful when I come back to it. - **Collaborate.** Real-time pair programming. The success criterion is whether the code I commit at the end is meaningfully better than what I'd have written alone. - **Verify.** Adversarial review of my work or the team's. The success criterion is whether real bugs get caught before they ship. The mistake teams most often make is using the wrong mode. Trying to delegate something that actually needs collaboration produces unusable code. Trying to collaborate when verify-mode is what's needed produces echo-chamber agreement instead of real review. Pick the mode deliberately. ## The handoff protocol The single discipline that keeps multi-agent work coherent: explicit handoff context between sessions. When one agent's work feeds into another's, you don't trust them to figure it out. You write a one-paragraph context dump and paste it into the next session. For example: morning audit agent produces a list of 14 places where retry logic is missing. I review the list, decide which 6 are worth fixing, and write a paragraph: "We're going to add retry logic to these 6 functions: [list]. Use exponential backoff with jitter, max 3 retries, log each retry at `warn` level. Match the pattern in `lib/external/retry.ex`." That paragraph goes into a fresh agent session for the implementation work. The agent doesn't see the original audit. It sees the curated context. This is the core of working with agents at scale: *you* are the context router, deciding what each session needs to know. ## The trap that produces slop Most teams who report disappointing results from agentic AI are running into the same failure mode: agents that look productive but produce slop. The signature: lots of code is committed, the team feels productive, and three weeks later the production codebase is a mess of subtly broken patterns nobody can fully explain. The cause is almost always one of three: - **No review discipline.** Agent-generated code is going into the repo without a human pass. - **Mode mixing.** Delegate-mode work being treated as collaborate-mode by the team, so nobody is closely engaged with the output. - **Context starvation.** Agents being asked to do work without enough context to do it well, producing plausible-but-wrong code. All three are solvable. None of them are solved by "use a better model." They're solved by team-level discipline about how AI work enters the codebase. Without that discipline, more agents produces more slop. With it, the throughput gain is real and durable. ## The takeaway Agentic AI is a force multiplier in engineering when treated as a workflow change rather than a tool swap. Four to seven sessions in parallel sounds like a lot until you recognize that most of them are running asynchronously while you do other work — and that your role across all of them is the same: provide context, scope tightly, review carefully. The teams shipping 40–55% faster aren't typing more. They're directing more. That's the new bar. Most engineers will get there in 2026. The ones who get there first will have a meaningful, compounding advantage for the next two or three years before everyone catches up. ## Tooling and cost The economics of running 4–7 agent sessions a day are easy to get wrong. A few practical notes: - **Pay for the paid tier.** Free-tier rate limits will produce flow-state interruptions multiple times a day. The $200–600 per month for unlimited Claude Code, ChatGPT Pro, and Codex usage is the highest-ROI line item on your engineering bill at this stage. - **Don't run the same task in two tools "for comparison."** Pick one. Comparison runs sound disciplined and produce cognitive overhead that erodes the throughput gain. - **Track cost per outcome, not cost per session.** A $4 agent session that produces a working feature is cheaper than a $0.40 session that produces noise. Most teams track the wrong number. - **Agent context is the expensive resource, not tokens.** Spend more time on writing good prompts and feeding the right files. Don't optimize for shorter prompts; optimize for clearer ones. One closing note on team adoption. The workflow above is what I run as an individual. Scaling it to a team adds a coordination problem — multiple engineers spawning agent sessions in shared codebases, occasionally producing conflicting changes if not careful. The pattern that's worked: agents work on isolated branches, never main, and the human engineer is the one merging up. Treat agent sessions like junior engineers' branches, with the same review discipline. ## Read this next - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — The team-level framing of the same workflow. - [**When to Trust an Agent and When to Step In**](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) — The discernment side of the discipline above. - [**How I'd Hire a Staff Engineer at an AI Startup**](https://sublimecoding.com/blog/how-id-hire-a-staff-engineer-at-an-ai-startup) — The AI-fluency bar I'd hire against. - [**Prompt Skills, Not Claude: Four Rules from Anthropic's Engineers**](https://sublimecoding.com/blog/prompt-skills-not-claude) — Why prompt-engineering moved from the chat to the folder. --- ## 4,154 Commits in Six Months With AI Agents URL: https://sublimecoding.com/blog/4154-commits-six-months-ai-agents Published: 2026-07-06 Tags: AI, agents, productivity > **TL;DR:** In six months of 2026, I’ve committed 4,154 times to a single repo. My previous average was 500–1,500 commits per year, across everything. The project — [EnergyConnect](https://energyconnecter.ai), a bulk liquid and chemical marketplace — went from zero to production in sixty days. I’ve written almost none of the code by hand. I guided every line of it, reviewed every line of it, and broke things I didn’t catch at a pace I’ve never broken things before. Lines of code is a terrible metric. Commit count is a terrible metric. But zero to production in sixty days with a team this small is not a metric — it’s a receipt. ## The screenshot that made me write this I pulled up my GitHub contribution graph a few days ago and genuinely didn’t believe it. 4,154 contributions in 2026. We’re barely past the halfway mark. My entire 2024 was maybe 1,200. My 2023 was somewhere around 800. And those were years I’d have called productive — I was shipping features, closing tickets, doing the work. This year I’ve shipped more code than the previous four years combined, and I have written almost none of it by hand. The commits are real. They compile, they pass tests, they run in production, and customers are using the software behind them. But what they represent is fundamentally different from what a commit meant to me two years ago, and I think it’s worth being honest about what changed, what got better, what got worse, and what it means if you’re watching your own commit graph spike and wondering whether any of it is real. ## What 4,154 commits actually are Let me kill the first objection before it forms: no, this is not commit noise. There’s a reasonable person’s version of that argument — that agents commit more often, in smaller chunks, and inflate the count relative to how a human would batch work into larger, less frequent commits. That’s true. But I’d argue that’s a feature, not an artifact. When I was writing code by hand, a typical commit was a bundle. “Implement carrier onboarding flow” — a commit message covering 400 lines across eight files. If something broke in that bundle three weeks later, good luck bisecting it. The commit message told you nothing about which of those eight files was the problem. The agent commits differently. It commits after each logical change — a function, a migration, a test, a style fix — with a message that describes exactly what changed and why. We’ve instructed it to do this deliberately, and we’ve instructed it to attach Linear ticket IDs to every commit. The result is that our git history is, for the first time in my career, actually searchable. It’s documentation. It’s the changelog we always said we’d write and never did. So yes, the commit count is higher. It’s higher because each commit is more precise, more traceable, and more useful than the bloated bundle commits I used to write by hand. The 4,154 isn’t noise. It’s granularity we never had before. ## Zero to production in sixty days The project those commits live in is [EnergyConnect](https://energyconnecter.ai) — a SaaS marketplace for bulk liquids and chemicals. It sits as middleware between carriers, brokers, suppliers, and end customers. The kind of platform that has a dozen user roles, complex multi-path forms, regulatory requirements, and integration points with external systems. We went from an empty repo to production in sixty days. We’re now at roughly 120 days, and the project is more mature than some codebases I’ve worked on for years at previous companies. We’re close to shipping a mobile app — the first mobile app I’ve ever built. I would not have had the confidence to say “yes, I can do that” twelve months ago. Not because I couldn’t learn it, but because the calendar math didn’t work. Learning a new platform, a new set of conventions, a new build system, and actually shipping something production-grade — that’s a quarter of focused work for an experienced mobile developer. I am not an experienced mobile developer. But I can describe what I want, review what comes back, and iterate until it’s right. That loop is fast enough now that the calendar math works. The codebase has 1.5 million lines across it. I need to be clear about what that means: it’s not 1.5 million lines of hand-crafted, artisan code. It includes generated code, vendor code, scaffolding, configuration, and all the other things a real project accumulates. Lines of code is a terrible metric for measuring value — always has been. But it is a useful metric for measuring *surface area*, and surface area is where the real story gets complicated. ## More code, more blast radius Here is the thing nobody talks about when they celebrate AI-assisted shipping speed: every line of code you ship is a line of code you have to maintain, debug, and answer for when it breaks at 2am. I’ve [written about this at the org level](https://sublimecoding.com/blog/ai-wont-shrink-your-team) — velocity creates surface area. This is what it looks like from inside one codebase. EnergyConnect has a carrier onboarding flow with six completion paths — different carrier types, different document requirements, different approval chains. An agent will nail the path you described in your prompt and silently break two others. We had a week where every fix to the hazmat carrier flow introduced a regression in the general freight flow. The agent had no persistent memory of the other paths unless we explicitly described them or it read the whole file. [Memory helps](https://sublimecoding.com/blog/ai-memory-outlives-any-agent) — once you correct a mistake, a well-configured agent won’t make the same one twice. But you have to catch it the first time, and catching it the first time in a codebase growing this fast is genuinely hard. This is the part of the AI productivity story that the conference talks skip. Yes, you can ship faster. But “faster” includes faster bugs, faster regressions, and faster accumulation of code that nobody has deeply read. The velocity is real. The risk scales with it. ## The snake eating its own tail Our review workflow is the part that sounds absurd until you realize there’s no alternative. We use adversarial review agents — agents specifically configured to be skeptical, to look for the exact failure patterns we’ve learned to expect, to challenge assumptions and check edge cases. Then we do a manual pass — actually load it up, click through it, look at it with human eyes. Then a final review before production. There is no version of this where a single human reviews 4,154 commits by hand. The math doesn’t work. At five minutes per commit — and that’s fast for a real review — you’re looking at 345 hours of review. That’s two months of full-time work just reading diffs, doing nothing else. So the AI reviews the AI’s work. And I review the AI’s review. And yes, this is the snake eating its own tail. I’m aware. The question is whether the alternative — shipping less, or shipping without review — is better. It isn’t. The adversarial review catches real bugs. Not all of them, but enough that the process has earned its place. The [specific patterns worth naming](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong) — the confident wrong answer, the silent regression, the context-window cliff — repeat predictably enough that you can build review agents around them. The bugs that get through are the ones where both the writing agent and the reviewing agent share the same blind spot — usually an implicit assumption about business logic that neither one has enough context to question. That’s where the human still earns their keep. Not in reading every line, but in knowing which lines to look at — the ones where the business context is subtle enough that an agent’s confident “looks good” means nothing. ## What I gained I can ship things I’ve never built before. That’s the headline gain, and it’s not small. A mobile app. A complex multi-tenant marketplace. Integration patterns I’d have spent a week researching and another week implementing — done in an afternoon with an agent that’s read the docs I haven’t. I write JavaScript as confidently as I write Elixir as confidently as I write Go. Not because I learned three more languages, but because syntax stopped being the bottleneck. I can pick up where someone left off in a codebase I’ve never seen, in a language I’ve barely used, because the skill that matters now is problem decomposition and review — not remembering whether the function signature takes the error first or last. The agents are better at certain kinds of work than I am. Boilerplate, scaffolding, the tedious mechanical work that used to eat the first two hours of any feature — they do it instantly and correctly and I never have to think about it again. That’s real time back. That’s afternoons I spent on architecture instead of plumbing. And the ticket hygiene — I almost can’t believe I’m saying this — is genuinely better. The agents attach ticket IDs to commits, update statuses, close tickets with references to the commits that resolved them. The project management layer that every team says they’ll maintain and nobody does? It’s maintained. Because the thing doing the work doesn’t get tired of process. ## What I lost There was a simplicity in writing code by hand that I miss more than I expected to. Not the syntax — I don’t miss semicolons or arguing about bracket placement. I miss the *solving*. The specific feeling of staring at a problem, tracing through the logic in your head, and arriving at a solution that you built from nothing. The agent gives you the solution in four seconds, and it’s usually right, and you saved an hour, and you lost something you can’t quite name. Syntax used to be interesting to me. It isn’t anymore, because we don’t need to care about it. That’s probably good — it’s probably the same as how accountants stopped caring about long division when spreadsheets arrived. But it was a thing I enjoyed, and it’s gone, and I notice its absence. The role shift is real. I am not a software engineer in the same way I was eighteen months ago. I am a director and reviewer of software engineering, performed at high speed by something that is very good at the mechanical parts and genuinely bad at the judgment parts. The judgment is what I provide. The judgment is, I think, all I provide now. And the uncomfortable question underneath all of it is whether judgment alone — without the reps of actually writing code — degrades over time. I don’t have an answer yet. Ask me in a year. ## The math changed At the end of the day, business is dollars and cents. Money in, money out. A solo developer — or a very small team — shipping a production marketplace in sixty days is a different economic equation than the same project taking six developers nine months. The math has changed. But the math has changed in both directions. You can ship more, faster, cheaper. You also have to review more, catch more, maintain more. The blast radius of your output has grown, and if you don’t invest in the review layer — adversarial agents, manual verification, real QA — you’re not shipping faster, you’re *failing faster*. The commit count goes up either way. What determines whether those commits are an asset or a liability is whether you respected the blast radius. 4,154 commits. More than the previous four years combined. People are using the software. The bugs are real, the features are real, and the pace is real. I just didn’t write most of it. --- **If you’re building with agents too:** I wrote about [what agents get wrong](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong) most often, [why AI won’t shrink your team](https://sublimecoding.com/blog/ai-wont-shrink-your-team) the way you think, and [what my daily agentic workflow](https://sublimecoding.com/blog/my-daily-agentic-ai-workflow) actually looks like — the boring, repeatable version of what this post describes. --- ## Your Coding Agent Has No Reason to Write Good Code URL: https://sublimecoding.com/blog/why-ai-agents-write-unmaintainable-code Published: 2026-08-01 Tags: AI, agents, engineering > **TL;DR:** The pitch of the moment is that if you engineer enough harness — better orchestration, more review bots, tighter sandboxes — you can delete code review and let agents run the factory unattended. I don’t think that works, and I don’t think it’s a skill issue. Coding models are trained against a reward that fires on one question: did the tests go from red to green without breaking anything else. Nothing in that signal grades design. A test verdict costs seconds; bad architecture bills you in months, which means there’s no fast oracle for maintainability, which means it can’t be rewarded during training, which means it was never learned. Harness engineering raises the floor. It does not move the ceiling, because the ceiling was set during training. ## The pitch you’ve been hearing all year The argument runs like this, and it’s seductive because most of it is true. You are the bottleneck. The models are good enough. Code is effectively free now. So stop hand-crafting software and start running a factory: specs go in one side, tested and deployed software comes out the other, and the humans move up a level to writing specifications and curating test scenarios. The most committed version of this shipped in February 2026, when StrongDM’s AI team published a charter for what they call a software factory. Two rules, stated plainly: [code must not be written by humans, and code must not be reviewed by humans](https://simonwillison.net/2026/Feb/7/software-factory/). Agents write it, agents test it, agents ship it. The lights stay off because nobody’s on the floor. I want to be fair to that team, because they are not naive. They ran directly into the obvious failure — that agents grading their own work will cheat, hardcoding the value a test checks for — and they built serious infrastructure to close it, including [behavioral clones of every third-party service their software touches](https://simonwillison.net/2026/Feb/7/software-factory/) so the tests run against something with teeth. That is not a blog post. That’s real engineering. But the pitch that’s circulating downstream of it has been flattened into something much dumber: that the last human step, the one where somebody reads the code, is a legacy ritual you can automate away with enough linters and a prompt containing the word “adversarial.” And I’ve watched enough agent output go by to be confident that’s wrong in a specific, mechanical way that no amount of tooling fixes. ## The receipts are in, and they’re not good We now have data instead of vibes, and the data is uncomfortable for everyone selling velocity. Faros AI published a study in April 2026 — [22,000 developers across more than 4,000 teams](https://www.faros.ai/blog/ai-acceleration-whiplash-takeaways), two years of telemetry pulled from trackers, IDEs, static analysis, CI, version control, and incident management. Output went up, exactly as promised: task throughput per developer up 33.7%, epics completed per developer up 66%. So did everything else: - Bugs per developer up **54%** - Incidents-to-PR ratio up **242.7%** - Median time in review up **441.5%** - Pull requests merged with **no review at all** up **31.3%** - Code churn up **861%** Read those last two together, because that’s the whole story in two lines. Review time exploded, so review got skipped. Not because anyone decided to skip it — because a review queue that grows faster than the humans reading it has exactly one release valve, and that valve is the merge button. This is correlation, and I’ll say so plainly. It doesn’t prove the agents caused the incidents. But it matches what I see week to week, and it matches the shape of the failures I’ve catalogued from my own sessions in [the agent bugs I catch every week](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong). Directionally, I believe it. ## “You’re holding it wrong” — you’re not The standard response to all of this is that you’re doing it wrong. Not enough context engineering. Not enough tokens. You’re still reading the diffs like it’s 2023, grandpa. Let go. I want to be careful here, because I’m not the guy who tried agents for a weekend and wrote a thinkpiece. I’ve [committed 4,154 times in six months](https://sublimecoding.com/blog/4154-commits-six-months-ai-agents) with agents doing nearly all the typing, on a product that went zero-to-production in sixty days and has customers on it. I like these tools. I’m not going back. So this isn’t a warning from the sidelines. It’s the opposite: the more time I spend inside the loop, the more convinced I am that the thing people are trying to prompt their way around isn’t in the prompt at all. It’s in the weights. And you cannot fix a training problem with a harness. ## What actually gets graded To see why, you have to look at what “make the model better at coding” means concretely. The loop is roughly: generate a bunch of agent traces attempting a coding task, score each trace against a verifier, then update the weights so high-scoring traces get more likely and low-scoring ones get less likely. Repeat several million times. That’s reinforcement learning with a verifiable reward, and it’s the machinery behind essentially every serious coding model shipping today. Everything hinges on the verifier. So what does it check? Take the canonical setup, the one most of this work descends from — [SWE-bench](https://www.swebench.com/) and the training harnesses built on top of it. A task is scraped out of a real repository — an actual bug, an actual fix. The model gets the repo checked out to the commit right before the fix landed, plus the issue text. It writes a patch. Then the grader throws away any edits the model made to test files (yes, that’s necessary), applies the benchmark’s own test patch, and runs the suite against two lists: - **FAIL_TO_PASS** — the tests that were broken. Do they pass now? - **PASS_TO_PASS** — the tests that already worked. Are they still fine? Both green, reward is 1. Anything else, reward is 0. That’s it. That’s the entire signal, and the research literature is direct about the consequence: execution-based feedback [cannot distinguish between trajectories that yield the same outcome](https://arxiv.org/pdf/2512.21919). Two patches, one clean and one a disaster, both turning the suite green, are literally the same number to the optimizer. So ask what the model learns from millions of iterations of that. It learns to make tests pass. It learns it extremely well — that’s why these tools work at all. What it never learns, because it is never once asked, is whether the codebase is better or worse for what it just did. There is no penalty for a fix that works by wrapping the call site in a try/rescue and swallowing the error. There’s no penalty for adding the eleventh special case to a function that needed refactoring at the third. There’s no penalty for solving the ticket in a way that makes the next ticket harder. **There is no penalty for eroding maintainability.** Not a small one. Zero. The lived version of that, at full length and with the receipts, is [Peter Welch's essay refiled by someone who now has robots](https://sublimecoding.com/blog/programming-sucks-now-with-agents). ## The asymmetry that makes this hard to fix The obvious response is: fine, add quality to the reward. People are trying. It’s harder than it sounds, and the reason is structural, which means it won’t age out with the next model release. Running a test suite gives you a clean verdict in seconds. That speed is the entire reason RL can afford millions of iterations — the oracle is cheap, so you can consult it constantly. Now price the oracle for architecture. The cost of a bad design decision doesn’t materialize in seconds. It shows up the first time somebody opens that file for a one-line change and discovers the one line lives in eleven places. It shows up in the review that takes three days because nobody can hold the blast radius in their head. Eventually it shows up as the problem I laid out in [the onboarding playbook for an agent-heavy codebase](https://sublimecoding.com/blog/onboarding-agent-heavy-codebase): a new person, or a new session, unable to form an accurate model of how the system fits together. That bill arrives in weeks, months, sometimes years. **Tests answer in seconds; architecture answers in quarters.** You cannot put a quarters-long feedback loop inside a training loop that needs to run millions of times, and no clever engineering collapses that gap, because the gap is what maintainability *means*. Maintainability is defined by future change. It is unobservable in the present. That’s not a tooling limitation — it’s the definition. Which lands us somewhere uncomfortable: the single property that determines whether your codebase survives contact with an agent for a year is the one property the training process structurally cannot grade. ## Why more review bots raise the floor and not the ceiling Here’s where the harness argument comes back, and it deserves a real answer. More review agents do help. I run them. They catch the dumb stuff — the unused import, the missing nil check, the obvious injection, the test that asserts nothing. That’s genuine value and I’d keep them even if the rest of this post is wrong. But look at what a review agent *is*. It’s the same class of model, running the same weights, asked to grade instead of generate. Its ability to recognize bad design is bounded by whatever design sense survived training — which is the exact thing we just established was never rewarded. And that leads to the trap sitting under every “just add an AI reviewer” plan: **a model that could reliably tell good code from bad would have written the good version in the first place.** The generator and the judge are drinking from the same well. Stacking more judges gets you more coverage of the failures the model already recognizes. It does not get you a new capability neither model has. So the floor comes up. The ceiling doesn’t move. And on a long-running codebase, the ceiling is what you’re actually betting. There is a second way to reach that same ceiling, and it long predates the models: [the theory a program is built on never lived in the code](https://sublimecoding.com/blog/code-was-never-the-hard-part-theory-building), so an agent has no way to read it back out. ## The frontier is moving, slowly, and honestly I want to be careful not to argue this is impossible. It isn’t. It’s unsolved, and the hype is running well ahead of the discipline — those are different claims. The most serious attempt I’ve seen is Cognition’s [FrontierCode](https://cognition.com/blog/frontier-code), and it’s interesting precisely because it refuses the pass/fail frame. Instead of asking “did the tests go green,” it asks whether a maintainer would actually *merge* the patch. Tasks were built with open-source maintainers at 40-plus hours of input each, and the rubric grades regression safety, scope, cleanliness, test correctness, and maintainability as separate dimensions, each scored on its own, instead of collapsing everything into one bit. They report an 81% lower false-positive rate than the previous generation of benchmarks. That is the right direction. It’s the first evaluation I’ve seen that takes seriously the idea that a patch can be correct and still be bad. It’s also still a rubric applied by models and humans, with all the cost and subjectivity that implies, and it doesn’t escape the well problem above — it just puts more careful humans upstream of it. I’d call it real progress and I would not yet bet a production codebase on the models it produces. Both of those can be true. The other honest position is: maybe a future model just gets this and the whole problem dissolves. Maybe. If you want to hold your codebase’s fate against that bet, that’s a choice you’re allowed to make. I’ve got systems in production this quarter. ## What I actually do instead None of this argues for using agents less. It argues for putting the human effort in the one place the training process left empty. Four things, all of which I run daily. **Front-load the alignment, don’t back-load the cleanup.** The oldest trick in the trade still works and works better than ever: time spent agreeing on the approach before generation is worth many multiples of time spent untangling afterward. The generation step compressed hard; deciding *what to build and how it should be shaped* didn’t compress at all, and that step is now the highest-leverage hour in the week. A well-shaped change reviews fast. A sprawling one doesn’t review at all — see the 441% number. **Keep architecture human-owned, explicitly.** The agent proposes the module boundary; a person decides it. This is the same principle behind [never letting the agent be the only thing between a change and production](https://sublimecoding.com/blog/amazon-let-the-ai-drive), applied one level earlier — at the design decision instead of the deploy. Structural decisions are precisely the ones with a feedback loop too slow for the model to have ever learned from. **Write the constraints down where the agent reads them.** Most of what a senior engineer knows about a codebase is unwritten, which means the agent doesn’t have it and can’t infer it. The fix is a file it reads every session — [the CLAUDE.md entries that earned their place after 50 commits](https://sublimecoding.com/blog/claude-md-after-50-commits): the boundaries, the patterns this repo actually uses, the things that look reasonable and are forbidden here. It doesn’t give the model design taste. It gives it *this* codebase’s design decisions, already made, which is the part it was never going to derive. **Spend your reading budget asymmetrically.** You cannot read everything at agent throughput, and pretending otherwise is how teams end up merging without review. So don’t distribute attention evenly — concentrate it where an error is expensive and irreversible, and let the mechanical stuff ride on tests and bots. Which is also why [TDD holds up unusually well with agents](https://sublimecoding.com/blog/tdd-claude-code-elixir): a test the agent has to satisfy is a constraint that survives the session, and it converts some of your review budget into something that runs automatically forever. ## The step you can’t delete The lights-off factory is a good story with one bad assumption inside it: that code review is a bottleneck to be automated rather than the only stage in the pipeline where anyone asks whether the system is getting better or worse. Every other step has a fast, cheap, automatable verdict. Does it compile, do the tests pass, did it deploy, is it paging anyone. Review is the odd one out precisely *because* it’s slow and human — it’s where the quarters-long question gets asked in the present tense, by the one participant who’ll still be maintaining this thing next year. Delete it and you don’t get a faster factory. You get the Faros numbers: more output, more incidents, more churn, and a codebase drifting toward the state where the only remaining option is the expensive one — throw it out and write it again by hand. You can move fast. I do, and the receipts are public. You just can’t skip the one step that nobody trained the model to care about. ## Read this next - [**The AI Coding Agent Bugs I Catch Every Week**](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong) — The close-up: eight failure patterns and the reflex for each. - [**When to Trust an Agent and When to Step In**](https://sublimecoding.com/blog/when-to-trust-an-agent-and-when-to-step-in) — Where to spend the review budget you actually have. - [**We’re About to Stop Making Senior Engineers**](https://sublimecoding.com/blog/stop-making-senior-engineers) — Who’s left to own the architecture if nobody reads the code. --- ## The Hard Part Was Never the Code. It Was the Theory. URL: https://sublimecoding.com/blog/code-was-never-the-hard-part-theory-building Published: 2026-08-11 Tags: AI, agents, engineering > **TL;DR:** Senko Rašić’s post calling “code was never the hard part” an insult to programmers hit the front page and drew a 400-plus-comment argument, and both sides of that argument were right, which is the tell that everyone was measuring the wrong thing. Peter Naur settled it in 1985: programming is building a *theory* of how some part of the world is handled by a program, and the source code is a lossy artifact of that theory, not the theory itself. That reframe explains why the phrase feels obviously true to some engineers and obviously insulting to others — they hold different amounts of theory. It also explains the thing 2026 actually broke. An agent produces text without theory. Naur’s word for a program whose theory is gone is *dead*, and the new failure mode is a program that was born that way. ## A phrase that started a fight You have heard some version of it, probably this month. *LLMs may be good at coding, but code was never the hard part.* Sometimes it’s dressed up — the hard part is figuring out what to build, the hard part is the requirements, the hard part is the people. Senko Rašić [pushed back on it hard](https://blog.senko.net/code-was-never-the-hard-part-is-an-insult-to-all-programmers), and his framing is the reason the post traveled: he read the phrase as a status claim rather than a technical one. “I believe that’s a gross insult to all programmers everywhere.” His arguments are the obvious ones and they land: if coding is easy, why were programmers in high demand at high salaries for years? Why is so much software so buggy? Why do *Clean Code* and SICP exist? And the turn back on the other camp — if deciding what to build is the genuinely hard part, why do so many product managers seem clueless? He refuses the craft-versus-customer binary entirely: *¿Por qué no los dos?* He ends where I’d end. Don’t outsource your understanding, judgment, empathy, and taste to a model. Then [the Hacker News thread](https://news.ycombinator.com/item?id=49222189) did something more interesting than agree or disagree. It split, cleanly, and both halves were credible. One camp said the phrase is being read too literally. Nobody means coding is *easy* — they mean coding was never the *bottleneck*. The thing that killed their last three projects was misalignment, a stakeholder who couldn’t prioritize, a requirement that changed after the build. Enterprise work in particular is mostly validation, SQL, and scheduling; the difficulty is pinning down rigid requirements with the edge cases considered, not expressing them. The other camp brought receipts in the opposite direction. Products die of bad code all the time. Memory leaks that make a paid tool unusable. Performance collapse that leaks users quietly for a year. Feature velocity that decays until nothing ships. That camp read “code was never the hard part” as a claim written by people who have never shipped anything where the code could actually lose. Both descriptions are accurate. Not “both have a point” in the diplomatic sense — both are literally, verifiably true about the work the person describing them does. So the disagreement isn’t about the profession. It’s about something the phrase can’t see. ## Naur’s answer, from 1985 Peter Naur — the N in BNF — wrote a paper called [“Programming as Theory Building”](https://gwern.net/doc/cs/algorithm/1985-naur.pdf) in *Microprocessing and Microprogramming* in 1985. Alistair Cockburn thought enough of it to reprint the whole thing as an appendix in *Agile Software Development*. It is nine pages, it is forty-one years old, and it is a better description of what just happened to our jobs than anything published this year. Naur’s claim is that we have the object of the work wrong. We think we produce programs. He argues we produce *theories*: > programming properly should be regarded as an activity by which the programmers form or achieve a certain kind of insight, a theory, of the matters at hand He is using “theory” in Gilbert Ryle’s sense — not a formal model, but the thing a person has when they can do something *and* explain, justify, and answer questions about the doing. You have a theory of your commute. You have a theory of how to arrange furniture in a room. It’s tacit, it’s held by a person, and it is not a document. And then the line that does the damage: The theory built by the programmers has primacy over such other products as program texts, user documentation, and additional documentation such as specifications. Primacy. The code is downstream. Documentation, in Naur’s words, is “an auxiliary, secondary product.” The real artifact is in someone’s head, and everything on disk is a partial, lossy projection of it. He gives three tests for whether someone actually holds the theory of a program. They’re worth memorizing, because they’re the most useful review checklist I’ve encountered and they predate every process framework you’ve been made to sit through: - They can **explain how the solution relates to the affairs of the world** it helps handle — which real-world aspect each part of the program text is matched to, and conversely, where in the text any given real-world activity lives. - They can **explain why each part of the program is what it is** — support the code with a justification, which finally rests on their own direct, intuitive judgment about what was relevant here. - They can **respond constructively to a demand for modification** — take a change the world now requires and know where it goes and what shape it should take. Notice what fails all three. A person who can read the code perfectly. A person with complete documentation. A person who wrote it three years ago and has forgotten. ## Naur’s two case studies are about you The paper’s evidence is two field reports, and if you’ve inherited a codebase you already know how they end. **Case 1.** Group A builds a compiler for language L. Group B needs a compiler for L + M, a modest extension. Reasonable plan: start from A’s compiler. B gets full documentation, annotated program text, extensive written design discussion, *and* personal advice from group A. Genuinely good conditions — better than yours. It doesn’t work the way you’d expect. In several major cases, B proposes changes that make no use of facilities already inherent in the compiler’s structure — facilities that were discussed at length in the documentation B had read. B’s instinct is to bolt on patches, and Naur says those patches would have destroyed the design’s power and simplicity. A’s members spot it instantly and propose a simple fix framed entirely inside the existing structure. Naur: > This is an example of how the full program text and additional documentation is insufficient in conveying to even the highly motivated group B the deeper insight into the design, that theory which is immediately present to the members of group A. The postscript is worse. Ten years on, after passing through more hands with no contact with group A, the compiler still had its original powerful structure visible — “made entirely ineffective by amorphous additions of many different kinds.” **Case 2.** A 200,000-line real-time industrial monitoring system, adapted per installation. The people who diagnose faults on it were involved from design and have worked on it full time for years. They diagnose almost exclusively from their ready knowledge of the system plus the annotated program text — and here’s the part that should stop you — they are *unable to conceive of any kind of additional documentation that would be useful to them.* Meanwhile the outside groups who have full documentation and full guidance keep filing problems that trace back to inadequate understanding, which the insiders clear up in a conversation. Not “the docs were bad.” The insiders couldn’t imagine a document that would help, because what they have isn’t the kind of thing a document holds. ## This is what the Hacker News thread was actually arguing about Now the split makes sense, and it isn’t a disagreement about programming. It’s two groups honestly reporting different theory-to-text ratios. If you build forms over a database — most enterprise work, and I’m not being snide, I’ve done years of it — the theory is genuinely thin. The requirements *are* most of the theory. Once you know the rules, the mapping to code is close to mechanical, and any competent person can pick it up from the text. For that work, “code was never the hard part” is just true. Getting eleven stakeholders to agree what the rule is was the hard part, and it took four months. If you build something with real invariants — a scheduler, a distributed state machine, a query planner, anything with a performance budget or a consistency guarantee — the theory is thick, most of it never got written down anywhere, and the code is its only durable trace. Tell that person coding is the easy part and you’ve told them the part of their job that took a decade to develop doesn’t exist. That’s the insult Senko named, and he’s right to name it. “Code was never the hard part” is not a claim about the profession. It’s an accurate report of how much theory your own work carries. Which is why the argument won’t resolve. Both sides are describing their jobs correctly and hearing a claim about everyone’s. ## The part Senko didn’t take, and 1985 did Here’s where this stops being a semantics fight and starts costing money. Senko’s conclusion is don’t outsource your judgment to AI. Correct, and too general to act on. Naur gives you the mechanism, in a sentence he wrote when the state of the art was punched-card-adjacent: > if viewed merely as a change of the program text and of the external behaviour of the execution, a given desired modification may usually be realized in many different ways, all correct. At the same time, if viewed in relation to the theory of the program these ways may look very different, some of them perhaps conforming to that theory or extending it in a natural way, while others will be wholly foreign Read that again with an agent in mind, because it is the cleanest description of the actual problem I have found anywhere. A coding agent samples from *all correct*. That set is large. Every member of it passes your tests, satisfies the ticket, and survives review-for-style. Only a fraction of that set conforms to the theory of your program, and nothing in the model’s inputs distinguishes them, because the distinguishing information was never in the repository. It was in group A’s heads. This is the same mechanism I’ve written about from the training side — [your agent has no reason to write good code](https://sublimecoding.com/blog/why-ai-agents-write-unmaintainable-code) because maintainability was never in its reward signal. Naur gets you there from the other direction, and I find his version more useful: it isn’t that the model is careless. It’s that the criterion is genuinely absent from the artifact. He says so explicitly — the judgment “cannot be reduced to any limited set of criteria or rules.” You cannot prompt your way to it, because if it could be written as a rule, it would already be in the docs, and case 1 says the docs weren’t enough. And Naur names the failure mode this produces. He calls it decay: “modifications made by programmers without a proper grasp of the underlying theory.” That’s group B’s amorphous additions. He wrote *programmers*, and I’m not going to pretend he was predicting anything — but swap that one word and the sentence describes a merged agent diff on a system nobody currently understands, which is the same failure arriving by a faster route. ## Born dead Naur’s conclusion is the one people quote, and the reason it stings is that he means it literally: The death of a program happens when the programmer team possessing its theory is dissolved. A dead program keeps running. It keeps producing useful results, and it keeps making money — death is not an outage. Death becomes visible only when a demand for modification arrives and cannot be answered intelligently. Everyone reading this has worked on a dead program. It’s the one where a two-line change takes three weeks and the estimate is a coin flip. Revival, Naur says, is not reading the code and not restoring the docs. It’s a new team rebuilding the theory. He doesn’t put a price on that, and I won’t invent one — but nothing in his account suggests it’s cheaper than the first build, and case 1 is a group with documentation, source, and the original authors on call still failing to get there. Here’s the 2026 version, and it’s new. Naur assumed every program was alive once — a team held the theory, then dispersed. An agent-built system can arrive without ever having been alive. Nobody dissolved. Nobody left. The theory was never constructed, because construction is what happens in a person while they work the problem, and the working was delegated. That inverts the economics everyone is currently quoting at you. Cheap to produce, expensive from then on, permanently — because you skipped the step that makes future changes cheap, and the bill starts arriving at change #2. It’s why [taking over a vibe-coded app](https://sublimecoding.com/blog/inheriting-a-vibe-coded-app) feels different from taking over a bad human codebase. A bad human codebase has a theory you can partially recover by archaeology, since [every insane codebase was a sane response to constraints you can’t see](https://sublimecoding.com/blog/insane-codebases-were-once-rational). The other kind has no theory to recover. There’s nothing behind the text. ## What this changes on Monday I use agents for most of my working day and none of this is an argument against them. It’s an argument about which step you are not allowed to skip. **Review for *why*, not for style.** Style review has lost most of its value in my own work — the output comes back tidier than what I’d have typed by hand, and editing it is cheap either way. Naur’s test 2 is the question that still has teeth: *why is this part what it is?* Ask it of the diff. If the answer is “the agent chose it” or “it passes,” you’ve reviewed the text and not the change. This is the same bar as verification, one level up: verification asks whether it does what we said; theory asks whether it does it in a way this system can survive. **Use the modification test as your real readiness gate.** Naur’s test 3 is a better signal than any interview or checklist. Hand someone a change request against a surface they claim to own, and watch whether they respond constructively or start pattern-matching. That’s the difference between someone who read the code and someone who holds the program. **Keep one accountable human per surface, and mean it.** Not a name in a CODEOWNERS file. A person who can pass all three of Naur’s tests on that surface, who is given enough time on it to have built the theory, and who reviews what the agents do there. This is the real constraint on how many agents one person can supervise — not tokens, not review throughput, but how many programs one human can hold a theory of. It’s fewer than you want it to be. **Record constraints, not decisions.** Documentation is a secondary product and it will never carry the theory, but it can carry the *inputs* — which is what group B was missing. “Single service because two people and a March deadline.” “This queue is FIFO because the client’s reconciliation assumes it.” Constraints are the recoverable part. **Stop treating prompt transcripts as documentation.** A session log is a secondary product of a secondary product. It records what was asked and what was emitted. It does not record why this shape and not the other four correct ones, because in most cases nobody decided that — the sampler did. **Let juniors build theory on purpose.** The pile of unglamorous work was never busywork; it was the mechanism by which theory got built, which is [why removing it quietly stops producing senior engineers](https://sublimecoding.com/blog/stop-making-senior-engineers). Onboarding is the same problem in miniature, which is why [onboarding into an agent-heavy codebase](https://sublimecoding.com/blog/onboarding-agent-heavy-codebase) needs a different shape than the one you used in 2019. And the honest limit, because I’d be selling you something otherwise: you cannot fully write a theory down. That’s Naur’s entire point and no practice above defeats it. Every one of these is damage control on an unavoidable loss. The only real defense is continuity of people who have spent real time on the real problem — which makes headcount decisions, retention, and how much you delegate to agents the same decision, whether or not anyone frames it that way in the meeting. ## Where I land Senko is right that the phrase is an insult, and I think he’s right for a reason he doesn’t quite state. It isn’t insulting because code is difficult. It’s insulting because it mistakes the artifact for the work. Typing was never the hard part — no serious person ever claimed it was. Building the theory was, it still is, and it is the one part of this job that has not gotten one bit cheaper since 1985. What changed is that we can now buy the artifact without doing the work. That’s a real capability and I use it daily. But you’re buying the projection, not the thing it was projected from, and the difference doesn’t show up in the demo. It shows up at change #2, when someone asks why it’s built this way and every person in the room, including the one who shipped it, has to guess. If you’ve got a system nobody can confidently answer questions about anymore — inherited, agent-built, or just old — that’s a theory problem before it’s a refactoring problem, and the sequencing matters. [Happy to talk it through](https://sublimecoding.com/consulting). --- ## Your Junior Dev Leans on the Agent. Good. URL: https://sublimecoding.com/blog/junior-dev-leaning-on-the-agent Published: 2026-08-12 Tags: AI, agents, engineering leadership, engineering > **TL;DR:** A first-time lead asked me what to do about the developer on his team — still in college — who builds almost entirely with an agent. The code works, but it’s rigid, unabstracted, and hard to read, and he’s worried the junior isn’t learning. The instinct is to make him use AI less. That’s advice with an expiry date, and it doesn’t fix the actual problem. The readability bar existed because editing code was expensive; it isn’t anymore. The bar that still holds is verification — does this do what we said it does, and does it survive being wrong. Aim him there. Then be honest about what he genuinely loses, because something real is lost and pretending otherwise doesn’t help him. ## The question Same new grad I wrote about in [Every Insane Codebase Was Once Rational](https://sublimecoding.com/blog/insane-codebases-were-once-rational) — a year into the industry, running a project and, for the first time, another person. His second question was about that person: One of his developers is still in college and leans heavily on AI for his work. It works. But the lead is seeing a lack of flexibility when functions need to change, a lack of abstraction, and code that’s hard to read. He’s been trying to curb it and it isn’t landing. He’s worried the junior isn’t learning what he needs to — bad for the junior’s future, bad for the team, because the lead wants to hand him larger components without worrying about quality. Every part of that observation is correct. The diagnosis that follows from it is the part I’d push back on. ## The advice with an expiry date The instinct is to tell him to use AI less and write more of it himself. I understand the instinct and I think it’s a losing position, for a reason that has nothing to do with whether agents are good. It’s how he’s going to build. It’s how you’re going to build. It’s how I build — I’ve barely written a line of code by hand in the last six months, and I’ve been doing this for thirteen years. That’s not a prediction about 2030, it’s a description of a workflow that already shipped [4,154 commits in six months](https://sublimecoding.com/blog/4154-commits-six-months-ai-agents). You haven’t hand-written an HTTP request in years either. Almost nobody does, outside of debugging or implementing the protocol itself, which is a different job. The abstraction moved and the skill under it stopped being load-bearing. That’s happened repeatedly — assembly, memory management, DOM manipulation — and the engineers who dug in each time didn’t preserve the craft, they just arrived late. So “use it less” spends your authority on a rule that reality is going to overturn, in front of a junior who can see it being overturned. You get one or two of those before your feedback stops carrying weight. Spend it on something that survives. ## The readability bar was a proxy for edit cost Here’s the part worth thinking through, because it’s the actual shift and it’s easy to mistake for lowering standards. Why did we demand readable, well-abstracted code? Not for aesthetics. Because **changing code was expensive and the expense was human attention.** Every future modification required a person to load the whole thing into their head, find the right place, and change it without breaking three other things. Readability was the tax we paid up front to make that cheaper later. Abstraction was insurance against having to make the same edit in nine places by hand. The unit cost of that edit collapsed. Need to change the function? The agent changes it. Needs a refactor to accommodate the change? It does the refactor, faster than the junior would have, and faster than you would have. The rigid unabstracted code the lead is worried about is genuinely worse to hand-edit — and hand-editing is the thing that stopped happening. That doesn’t mean structure is worthless. It means **structure is no longer where the risk lives**, and continuing to spend your review attention there is spending it on the cheap problem. The expensive problem moved. ## Where the risk actually went If you’re not grading style, what are you grading? Behavior. Specifically: does this do what we claimed, does it still do it after the next change, and what happens when it’s handed something we didn’t anticipate. That’s where quality lives now, and it’s not a soft standard — it’s a harder one, because it’s falsifiable in a way “is this readable?” never was. Concretely, what I’d ask of a junior building with an agent: **Tests the agent didn’t write from the same context as the code.** An agent that writes the implementation and the test in one pass will happily write a test that passes against its own misunderstanding. Make the test come from the requirement, not from the code. Different session, or written first — [TDD holds up unusually well with agents](https://sublimecoding.com/blog/tdd-claude-code-elixir) for exactly this reason. **Mutate the test and confirm it fails.** This is the cheapest quality check in existence and almost nobody runs it. Break the implementation on purpose. If the suite stays green, the test is decoration. A junior who does this once will never fully trust a green checkmark again, and that skepticism is worth more than any style rule you could teach him. **Test the boundaries, not the happy path.** Empty input, huge input, wrong type, the network call that hangs instead of failing, the duplicate submit. The agent’s default is the path it just imagined; the failures are always in the paths it didn’t. **Non-deterministic output needs its own approach.** If any part of the product calls a model, normal assertions don’t hold — [testing agent outputs is its own discipline](https://sublimecoding.com/blog/testing-ai-agent-outputs-exunit) and it’s a good, concrete thing to hand a junior who wants harder problems. **Resilience over elegance.** What happens when the dependency is down, the input is malformed, the job runs twice? Those questions have real answers and reviewing them teaches systems thinking, which is the thing you actually want him to have. Note what this does to the mentoring problem. “Make your code more readable” is taste, it’s arguable, and a junior can’t tell whether you’re right or just particular. “Break the implementation and show me the test catches it” is a task with a binary outcome. He can run it himself, and it’s either green or it isn’t. That’s a standard that teaches without requiring him to defer to you. ## The honest tension I’ve argued before that [coding agents have no structural reason to write maintainable code](https://sublimecoding.com/blog/why-ai-agents-write-unmaintainable-code) — the training signal fires on tests going green, nothing in it grades design, and no harness moves that ceiling. That post’s prescription was to spend your reading budget asymmetrically and keep architecture human-owned. I still hold that. This post adds the other half, which is where the budget comes *from*: the attention you’re currently spending on readability is the attention that should be spending on verification. Not “review less.” Review the other thing. And I have to argue against my own numbers here, because that same post cites the Faros AI study of 22,000 developers across 4,000+ teams — incidents-to-PR up 242.7%, code churn up 861%, median review time up 441.5%. That is not a picture of quality problems that stopped billing anyone. It’s a picture of them billing harder. But look at what’s actually in that bill. Churn is code rewritten shortly after it’s merged — the cost of the *edit*, which is exactly the cost that collapsed. Incidents are behavior wrong in production. Review time is humans reading. Two of those three are the thing I’m telling you to aim at, and the third is the queue that broke because review time exploded until people started skipping it. None of those three lines is “the abstraction was inelegant.” The bill went up on verification and human attention, which is the argument for moving your bar there, not for holding the old one harder. The caveat that survives all of it: the ceiling still bites where structure and correctness are the same thing. Data model, module boundaries, auth flow, anything with a public interface. There, review the design — a bad call there isn’t a style problem, it’s a one-way door. So the rule isn’t “never review structure.” It’s: review structure where structure is load-bearing, and review behavior everywhere else. Most code is everywhere else. ## What compounds: context, not keystrokes The biggest single factor I’ve seen in whether AI-heavy development gets better or worse over time isn’t the model. It’s whether the team is accumulating anything. At EnergyConnect we maintain what amounts to a second brain for the agents — thousands of lines of markdown the agents read and write. Not documentation for humans that agents happen to parse. Working knowledge: a file describing how our bidding process runs, a file on how the carrier-supplier relationship works, and a log. When an agent gets something wrong because it didn’t know two of those things were related, we correct it and write the correction down as an entry. Next time, it looks up “carrier,” finds the file, loads that context on demand, and doesn’t make the mistake. The learnings compound instead of evaporating at the end of every session. That’s the difference between a team that gets faster with agents and one that just generates more code. I’ve written up [how to build the wiki your agent actually reads](https://sublimecoding.com/blog/build-an-llm-wiki-for-your-agent) and [what belongs in CLAUDE.md after fifty commits with it](https://sublimecoding.com/blog/claude-md-after-50-commits), so I won’t repeat the mechanics here. For a lead with one junior, this is the highest-leverage thing you can hand him, and it’s the answer to “he isn’t learning”: have him own the knowledge base. Every time the agent gets something wrong about your domain, he writes the entry that prevents it. To write that entry he has to actually understand the domain — which is the learning you were worried he was skipping, arriving through a door he’ll walk through voluntarily because it makes his own work easier. ## The part where he’s right, and what it costs I don’t want to wave away the worry, because underneath it is something real. The pile of unglamorous work that used to build judgment is exactly the pile agents are best at, and [that’s a problem I’ve written about at length](https://sublimecoding.com/blog/stop-making-senior-engineers). It hits juniors first and hardest. The mitigation isn’t taking the tool away. It’s making him responsible for the judgment the tool can’t hold: - **He owns the domain knowledge base.** Every agent mistake about your business becomes an entry he researches and writes. He can’t write it without understanding the domain, and he’ll do it willingly because it makes his own next prompt work better. - He owns the test strategy, so he has to know what could break. - He owns the incident when his component fails, so he learns what production does to assumptions. None of that requires him to type the implementation. All of it requires him to understand the system, which is what “learning” actually meant the whole time. The typing was never the point. It was just the only delivery mechanism we had. ## The standard you’re enforcing might not be yours One more thing he raised, and it’s the same problem wearing a different hat: he’s finding it hard to hold a standard he isn’t fully sure of himself, worried about whether he’s doing it the way the industry does it, while both he and his dev are new to this. Your job as the lead is to fit the standard to *your* team, not to import it. A practice can be a real industry standard and still be wrong here — if it costs you more than it protects you, it’s a cost, and you’re allowed to change it. Same instinct as with the code: fix the pain point, and when something breaks, fix that. For a two-person team, less process is usually the right first guess. And a fair amount of the job is just listening — if you’ve hired well, the person doing the work will tell you what they need and how they want to build; you mostly have to ask and then actually hear it. Read [*Peopleware*](https://www.oreilly.com/library/view/peopleware-productive-projects/9780133440706/). First edition 1987. The third, in 2013, added six chapters on leadership, meetings, and distributed teams, and otherwise left the thing alone — and there hasn’t been a fourth in the thirteen years since. You’re managing people. That part hasn’t moved nearly as much as the tooling has. ## What to do this week - **Stop asking for less AI. Ask for a broken test.** Have him break the implementation and prove the suite catches it. One exercise, immediate result, teaches more about quality than a month of style review. - **Move your review attention.** Structure gets reviewed where it’s load-bearing — data model, boundaries, auth, public interfaces. Everything else gets reviewed on behavior. - **Separate the test’s context from the code’s.** Requirement first, or a different session. Same-pass tests validate the misunderstanding along with the code. - **Hand him the knowledge base.** Every agent mistake about your domain becomes an entry he writes. That’s how he learns the domain and how the team stops repeating errors. - **Cut one process you imported because it was standard.** If it’s costing more than it protects on a two-person team, it wasn’t a standard, it was a costume. If you’re leading a small team through this shift and trying to figure out what to actually hold people to now, [that’s a conversation I have often](https://sublimecoding.com/consulting). --- ## The 'Smallest Possible Slice' Heuristic for Shipping Complex Features URL: https://sublimecoding.com/blog/smallest-possible-slice-shipping-complex-features Published: 2025-11-10 Tags: engineering, productivity **Most "we delivered late" stories trace to one decision: the team scoped the first slice too big.** The team breaks down the feature into "frontend, backend, database changes, the new microservice." They estimate two weeks. Six weeks later, the database changes are merged but the backend isn't, the frontend is half-built, the microservice is on a feature branch that's now 800 commits behind main, and nobody can demo anything because nothing actually works end to end yet. This pattern is so common it's worth giving it a name. Most teams break work down by *layer* when they should break it down by *slice*. The corrective: a heuristic I've leaned on for the better part of a decade — the smallest possible slice that touches every layer. ## Why "MVP" is too vague to fix this "Just ship the MVP" is good advice in spirit and useless in practice. MVP gets defined by stakeholder negotiation: marketing wants this, product wants that, engineering says we can have either of two things by date X. The result is a feature that's sized to "what fits in a sprint" rather than to "what actually works end to end." The smallest-possible-slice heuristic is more specific. The first version of any non-trivial feature should be defined by these three constraints, in order: - It touches every layer the final feature will touch — frontend, backend, persistence, infrastructure. - It works end to end for one trivially specific case. - It can be deployed to production by Friday. Notice what's not on the list: completeness, polish, edge cases, performance. Those come later. The first slice exists to prove the wiring is correct. ## The vertical-cut rule Most teams default to *horizontal* decomposition. Sprint 1: schema changes and migrations. Sprint 2: backend API. Sprint 3: frontend. Sprint 4: integration. Each sprint produces something, but nothing is testable in production until sprint 4. The vertical cut runs the other way. Sprint 1: the simplest possible end-to-end version. The schema has one table with three columns. The backend has one endpoint that returns a hard-coded response if the input matches. The frontend has one input and one output. It works for exactly one case. Ship it. The reason the vertical cut wins: every sprint after the first is now *additive*. You're adding cases, polishing UI, expanding scope — but the core wiring is already proven. When the inevitable scope cut comes (and it always comes), you have shipping software to ship instead of four feature branches to merge. ## The deploy-by-Friday filter The cleanest forcing function for the smallest-possible-slice mindset is a question I ask the team every time we scope a new feature: *can we get the first version into production by Friday?* If the answer is "yes, but only the schema changes," the slice is too horizontal. Throw it out. Re-scope. If the answer is "yes, with one customer behind a feature flag, working for the simplest case," that's the right slice. Ship that, see what breaks in production, then iterate. If the answer is "no, even the first slice will take three weeks," the work is genuinely large and you need to break it down into smaller features, not smaller slices of one feature. Bigger problem, different conversation. The reason the Friday filter works is that it forces the team to find the small case. Most engineers are uncomfortable shipping something obviously incomplete. The Friday deadline overrides that instinct just enough to get the first slice out the door, and once it's out, the team's attitude shifts from "what should we build" to "what should we add next." ## A concrete example At Lavender, we shipped a new AI feature that recommended improvements to user-written sales emails. The first scoping pass from the team came back as a four-week project: prompt engineering work, a new evaluation pipeline, a UI component for inline suggestions, an analytics dashboard, an A/B testing harness. I rejected it. Re-scoped to: one user, one email, one suggestion type, one model call, one button. The button shows up on a hardcoded email body for one specific user account. Click it, get a suggestion, render it. No analytics, no A/B, no eval pipeline. That version shipped on the fourth day. It was visibly thin. It also *worked*, which we hadn't yet proven the original four-week design would. Over the next two weeks we extended it to all users, added the suggestion-type variations, layered in the eval pipeline, and turned on the A/B harness. The full feature was in production at three weeks instead of four, and we caught two architectural issues during the first week — issues that would have been brutal to fix at the four-week mark with everything already integrated. Total elapsed: roughly the same. Total risk: dramatically lower. The "smaller" first slice was paradoxically the faster path. ## When the heuristic breaks The smallest-possible-slice rule fails for one specific category of work: *research-shaped problems*. Things where you don't yet know what the right answer is, only that there's a question. Examples: training a custom model from scratch, designing a novel cryptographic protocol, exploring whether a certain optimization is even possible. You can't ship a thin vertical slice of a question. You have to do the research first. The way I handle these: time-box the research as a separate phase, with a defined exit condition ("by week three we will have a written go/no-go decision on training a custom embedding model"). Once the research phase exits, the implementation phase reverts to vertical slicing. The mistake to avoid: dressing up the research phase as a feature build. If you're scoping "build an MVP of the new model" when the real question is "is custom training viable for our use case at all," the team is going to drift. Be honest about which kind of work you're doing. ## The tactical checklist When a team comes to me with a feature breakdown, I run it through five questions: - Does the first slice touch every layer the final feature will touch? - Can it be deployed to production by Friday? - Is there at least one user (or test account) who will see something different on Monday? - If we shipped only this slice and nothing else, would it be embarrassing but functional, or non-functional? - What's the explicit list of what's *not* in the first slice? The fifth question is the one most teams skip and the one that prevents scope creep most reliably. Writing down what's not in the first slice locks the team into the discipline of shipping something thin. Without it, "just one more thing" creep extends the slice by 50% before code is written. ## The takeaway The most reliable way to ship complex features fast is to ship a thin one first and grow it. Most teams know this in principle. Most teams violate it in practice because the first thin slice always feels embarrassingly incomplete. The discipline is sitting with the embarrassment. Ship the thin slice. Watch it work in production. Then add the next slice. Repeat for as long as the work is generative. The teams that internalize this ship faster than the teams that try to scope the whole feature up front, every time. The teams that try to predict everything at sprint planning and then deliver in one big bang ship slower and ship buggier. The slice heuristic is what separates these two patterns. Use it. ## Convincing a skeptical team The slice heuristic is intellectually obvious and culturally hard. Engineers who've spent years on teams that scope full features upfront will resist shipping the embarrassingly thin first version. The objections are predictable. "*It's not ready for users.*" Right — that's why it's behind a feature flag with one allowlisted account. Nobody will see it except the team. "*We're going to have to throw away this code when we build the real version.*" Maybe. But the discarded code is rarely the expensive part. The expensive part is the architectural learning, and that's preserved regardless of whether you keep the code. "*The product team will think we're shipping garbage.*" Get product in the room when you scope the slice. Show them that the next slice ships next week. The thin version stops being garbage when it's framed as a milestone, not a shipped feature. The way I get past this with new teams is to show, not tell. Pick a feature, scope it the team's preferred way, then re-scope it as a thin slice. Walk through both timelines on a whiteboard. The thin slice almost always wins on calendar time, even when the team initially said it would take longer. After one or two demos of this, the team converts itself. ## The retro question that locks the habit in One question, asked in every retrospective for a quarter: *could we have shipped a smaller first slice of the work we did this sprint?* The answer is yes more often than the team initially thinks. Asking the question consistently trains the muscle. After a quarter of asking, the team starts asking it during sprint planning instead of in retro — and that's when the heuristic has actually been internalized. One additional pattern: the slice heuristic compounds with continuous deployment. If your team can ship to production multiple times a day, slicing becomes the default mode of operation rather than a discipline you have to remember. If your deploys are weekly or batched, slicing requires constant re-justification because each "small slice" feels like a wasted deploy slot. The teams that internalize the heuristic fastest are usually the teams that already have the deploy infrastructure to support it. If your team doesn't, fix that first — the slice discipline rests on it. ## Read this next - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — How modern engineering tooling shortens the slice cycle even further. - [**How I Triage a New Codebase in 90 Minutes**](https://sublimecoding.com/blog/triage-a-new-codebase-90-minutes) — The pragmatic discipline applied to onboarding instead of feature work. - [**From One Engineer to Fifteen**](https://sublimecoding.com/blog/from-one-engineer-to-fifteen-engineering-leadership) — Why slice discipline is partly a leadership problem, not just an engineering one. --- ## How to Manage a 4-Person Engineering Team Without Becoming a Manager URL: https://sublimecoding.com/blog/managing-a-four-person-engineering-team Published: 2025-10-27 Tags: engineering leadership, founders **A 4-person engineering team is the most overlooked unit of management in startups.** Big enough that the tech lead can't write all the production code. Small enough that hiring an EM kills velocity and adds a layer of communication overhead the team can feel within a week. Most startup engineering org charts skip from "founding engineer" straight to "Director of Engineering at twenty people" — and pretend the territory in between doesn't have its own playbook. It does. I've run 3–5 person engineering teams at PopSocial, at InsideTrack during a phase transition, and most recently across two fractional engagements with AI-native startups. The patterns hold. Five rituals that work at this size, three traps to avoid, and the signal that tells you it's time to evolve. ## Why 4 is the awkward number One engineer is a co-founder. Two is a duo. Three is a tight team where everyone communicates by osmosis. By four, osmosis breaks. Engineer A doesn't know what engineer C is working on, two of them ship overlapping changes that have to be reconciled in code review, and the lead starts feeling like they're spending half the day on coordination that didn't exist last quarter. This is the moment most founders panic and either hire an EM or default to the worst option: announce themselves as the EM and stop coding. Both miss the point. At four engineers, you don't need a manager. You need *cadence*. Four people running on shared rhythms perform like five-and-a-half. Four people without rhythm perform like three. ## The five rituals that work at this size ### 1. Weekly 1-on-1s, 30 minutes The single highest-leverage 30 minutes on your calendar. Three sections, in this order: what's blocking you, what's bothering you, what are you working on. The first two are non-negotiable; the third is often obvious from context and can be skipped. Why this works at 4 engineers specifically: the alternative is finding out your strongest engineer is unhappy via their resignation email. At 20 engineers, you need a layer of management to surface this. At 4, the layer is you, weekly, in 30 minutes. ### 2. Async daily check-in, in Slack, text only Three lines per engineer per day, in a single thread. What I shipped yesterday. What I'm shipping today. What's blocking me. No video. No meeting. Twenty seconds to write, two minutes for the team to read. The crucial constraint: *text only*. Standups by video at small scale are a tax. The same information conveyed in text takes a tenth of the time and creates a written record you can search later. The team I most recently ran did standups this way for a year and never once felt the lack of synchronous time. ### 3. Friday demos, 15 minutes Once a week, on Friday, the team gathers for fifteen minutes. Each engineer shows the most interesting thing they shipped that week. Could be a feature, a refactor, a bug fix with a great post-mortem, anything they're proud of. The point is not status reporting. The point is *visibility* — engineers seeing each other's work, picking up patterns, noticing where someone has built something useful that another engineer didn't know existed. At 4 engineers, this is also the highest-bandwidth moment of pure team identity in the week. ### 4. Quarterly written goal docs, one per engineer Twice a year is too infrequent at startup pace; weekly is theatre. Quarterly hits the right rhythm. One page per engineer: three goals, written by them, reviewed with you, signed by both at the end of the meeting. Three goals, not five. They span the three buckets of work: ship something hard, learn something specific, contribute to the team in a defined way. At the end of the quarter you sit back down, look at the doc, and have a real conversation about what happened. This is how you compound performance at small scale without bureaucracy. ### 5. Monthly retros: what's broken One hour, last Friday of the month, no calendar invite needed beyond the recurring slot. Single question: *what's broken about how we work?* Not what's broken in the code. What's broken in the process, the tools, the meetings, the deploys, the comms. You take notes. The next week, you fix the one thing the team most wanted fixed. The team feels heard, the process actually improves, and the next month's retro builds on a smaller list of complaints. After six months of this, the team's operating practices are visibly better than every other 4-person team you'll ever see. ## Three traps to avoid **Trap 1: Hiring an engineering manager at four engineers.** The marginal cost is enormous and the marginal value is small. The EM at this scale will inevitably either become a tech lead in disguise (in which case you should have just promoted internally) or spend their day creating process to justify their own existence. Wait until six engineers. Possibly eight. **Trap 2: The lead going full-time managing.** This is the most common pre-Series-A founder mistake. You stop shipping code, the team's velocity drops 30% within a month because the senior IC just left the keyboard, and you spend the dropped time on meetings the team didn't want anyway. The right load at this stage is 70/30 IC/management. Below 50/50 management, you're failing the team. **Trap 3: Rituals that drift into status meetings.** Every ritual on the list above can collapse into "tell me what you're working on so I know" if you're not careful. The signal a ritual has drifted: the engineers stop volunteering things, you start asking direct questions, and the meeting feels heavy. When that happens, kill the meeting that week. Bring it back next week with explicit re-framing. ## The transition signal at six engineers The model in this post breaks somewhere between five and seven engineers. The exact number depends on team-shape and product-shape but the symptoms are consistent: 1-on-1s start eating your whole Tuesday, the async standup thread is too long to read in two minutes, and the Friday demo runs over because four-out-of-six demos already feels rushed. The transition is to a sub-team structure: two ICs, two ICs, with a tech lead per group. You're now managing the leads, not the ICs. Different rituals, different cadence, different challenges — and a whole different post. The good news: getting the 4-person ritual stack right is the foundation everything that follows builds on. The teams that hit Series A with healthy engineering culture are the teams that ran disciplined rituals at four. The ones who skipped the discipline at small scale are the ones spending the year after Series A re-installing it under pressure. ## Hiring at the 4-person team size One topic the rituals don't cover: every hire at this size is a culture-level decision, not just a skill match. At twenty engineers a single wrong hire is a 5% problem. At four, the wrong hire is a 25% problem and you'll feel the consequences inside six weeks. The implication: the bar for hires 3, 4, and 5 should be unreasonably high. Not "great engineer." Not "really senior." It needs to be: "this person makes the team meaningfully better the day they start, in ways the existing engineers couldn't have produced themselves." Anything less and you're hiring someone who needs to be brought up to the existing team's level, which the existing team will resent at this scale. Concretely, what's worked for me at this size: - **Every existing engineer is a hard veto.** No exceptions. If any of the three engineers on the team has a strong reservation about a candidate, the candidate doesn't get hired, regardless of what the founder thinks. This kills political pressure and keeps the team's culture in their own hands. - **Take-home → trial week → offer.** A 3-day paid trial week between final interview and offer is the most predictive single signal at this scale. You're not hiring for resume; you're hiring for whether they fit the team's flow. - **The fifth hire is the one to slow down on.** Going from four to five is when team dynamics noticeably shift. Most founders rush this hire. The team performs better at four for an extra month than at five with the wrong addition. ## Remote vs in-person at this size Brief note because the rituals work differently in each setup. Remote 4-person teams need *more* structure, not less — the async daily check-in becomes load-bearing instead of nice-to-have, and the Friday demo is the single thing that holds team cohesion together. Skip the demo for two weeks in a row and you'll feel the drift. In-person 4-person teams can run looser. The async standup can be optional because the team is already overhearing each other's progress. The Friday demo is still worth doing but it has less work to carry. Hybrid is the worst of both worlds at this size. If you can choose one or the other, choose. If you can't, default to remote-first rituals — they degrade gracefully when half the team is in the room and the rest are remote. In-person-first rituals don't. One last note on cadence. Founders sometimes ask whether all five rituals run from week one with a new hire. The answer is yes — the new engineer joins the existing rhythm rather than the team adapting to them. New hires actually onboard *faster* when they're plugged into a working ritual stack on day one, because the rituals make the team's expectations legible. Skip the rituals during onboarding and the new hire spends three weeks figuring out norms that should have been transmitted in week one. ## Read this next - [**From One Engineer to Fifteen**](https://sublimecoding.com/blog/from-one-engineer-to-fifteen-engineering-leadership) — Where the 4-person rituals fit in the broader leadership arc. - [**The Pre-Series-A AI Startup Hiring Plan**](https://sublimecoding.com/blog/pre-series-a-ai-startup-hiring-plan) — Who you hire to build the 4-person team in the first place. - [**How I'd Hire a Staff Engineer at an AI Startup**](https://sublimecoding.com/blog/how-id-hire-a-staff-engineer-at-an-ai-startup) — The single hire that levels up the team you already have. --- ## How I'd Hire a Staff Engineer at an AI Startup URL: https://sublimecoding.com/blog/how-id-hire-a-staff-engineer-at-an-ai-startup Published: 2026-02-23 Tags: hiring, staff engineer, AI startups, engineering leadership **The title "Staff Engineer" means three different things at three different companies. At an AI startup pre-Series-A, only one of those three is what you actually need.** I've been on both sides of the staff-engineer interview, hiring for the role at Lavender and BlockFi, and being interviewed for it more times than I want to count. The pattern I see most consistently in misfires: the company hires a staff engineer who's calibrated for a different flavor of "staff" than what the company actually needs, and either the engineer leaves within twelve months or the team works around them. If you're hiring a staff engineer at an AI startup pre-Series-A, here's the interview process I'd run, and the calibration I'd hold to. ## The three flavors of staff engineer The title is overloaded. The three distinct shapes: - **The systems architect.** Designs platforms, sets technical direction across teams, owns the architectural roadmap. Often doesn't write much code. Strongest at large companies with multi-team coordination problems. - **The principal IC.** Writes the hardest code on the team, owns the most-load-bearing parts of the codebase, mentors senior engineers. Hands-on. The "tech lead, but better." - **The deep specialist.** Single-domain expert — distributed systems, ML infra, cryptography, real-time graphics. The team needs them when the problem requires their specific expertise; otherwise they're slotted into general work and underperform. The flavor an AI startup pre-Series-A needs is **flavor 2: the principal IC.** You don't have multi-team coordination problems yet. You don't have a single domain so deep that a specialist is required. What you have is a small team that needs someone who can hold the entire codebase in their head, take the hardest features, and pull the senior engineers up. If a candidate's resume reads like flavor 1 (systems architect) or flavor 3 (deep specialist), they're not wrong as engineers. They're wrong for this role. Calibrate the funnel for principal-IC type and reject hard against the others, even if their pedigree is impressive. ## The screen: not LeetCode The first 30 minutes with a staff candidate should not be a coding question. By the time someone has reached staff level, you can confirm they can code through their work history, their code samples, and the take-home. The 30-minute screen is for two questions: - **Walk me through the most technically difficult thing you've shipped.** Listen for: depth of ownership, awareness of tradeoffs, ability to talk about failure modes, presence or absence of grandiosity. A staff engineer should have at least one or two "I owned this end-to-end and here's where it nearly went wrong" stories. - **What would you change about how engineering operates at the last company you worked at?** Listen for: opinion, not complaint. A staff engineer should have a clear, articulated point of view about engineering practice. If they don't, they're senior, not staff. If both answers are strong, move them to the take-home. If either is weak, decline immediately. Do not waste your team's interview hours on a candidate who can't pass these two. ## The take-home: should it exist? Yes. With caveats. Take-homes are controversial. The argument against: they're disrespectful of senior candidates' time, the signal is noisy, and the strongest candidates won't do them. I've heard all the arguments and I still believe in take-homes for staff hires, with three constraints: - **Two hours, hard cap.** If a candidate puts in eight hours, you're getting eight-hour signal — useless for calibrating against the actual job, where they'll have less time. - **Realistic problem, not algorithmic puzzle.** Build a small CLI tool that solves a real product problem. Wire two real APIs together. Implement a small retry policy with backoff. Things you'd actually ask them to do in week one. - **Pay for the candidate's time.** Not a lot — $200 for two hours. Sends the message that you respect their time, and it filters out candidates who have so many options they'd rather not bother. What I evaluate in the take-home: code clarity, naming, error handling, tests if they wrote any, and the README. The README is half the signal. A staff candidate's README should explain what they built, what they considered, what they cut, and why. If the README is missing or one paragraph, you've learned something. ## The interview loop: four rounds After the take-home, four rounds. Each one tests something specific. ### Round 1: Take-home walk-through (60 minutes) The candidate explains their take-home submission. You ask: "Why this approach?" "What did you not do, and why?" "What would you do if you had a full day instead of two hours?" "How would you test this in production?" You're looking for: tradeoff awareness, the difference between "I shipped it" and "I shipped it and here's the production-readiness gap." ### Round 2: Open-ended system design (60 minutes) Pick a system close to what the company actually builds. "Design the AI evaluation pipeline for a chat product." "Design the agentic tool-use authorization layer." Give the candidate 60 minutes; they drive, you ask follow-ups. You're looking for: ability to ask clarifying questions before designing, awareness of what they don't know, ability to scope the design to the actual problem (not the perfect-world version), and willingness to push back when your hypothetical doesn't make sense. The single biggest red flag in this round: a candidate who immediately sketches the "right" architecture without asking who the users are, what the load looks like, what the failure mode is. Senior engineers do that. Staff engineers don't. ### Round 3: Live code review (60 minutes) You hand the candidate a 200-line PR from your real codebase (or a contrived equivalent). They review it. You watch. You're looking for: what they comment on, what they miss, how they phrase the comments, whether they catch the bugs you planted, whether they suggest stylistic changes that don't matter and ignore the substantive ones. This round is the single most predictive signal I've found for staff-level performance. The way an engineer reviews other engineers' code is the way they'll show up to your engineering culture. If the review is sharp, kind, and substantive, you've found a staff engineer. If it's nitpicky, vague, or ego-driven, decline. ### Round 4: Judgment / leadership (60 minutes) This is the round most companies skip. They shouldn't. You walk the candidate through three to four scenarios pulled from real situations at your company: - "A junior engineer pushes a hot fix to production at 11pm Friday without code review. Walk me through what you do." - "The CTO wants to migrate from Postgres to a new vector database. You think it's premature. How do you handle the conversation?" - "Production is down. The on-call is in over their head. You're not on call. What do you do in the next 30 minutes?" - "A peer engineer is consistently producing low-quality work, and the eng manager isn't acting on it. Your move." You're looking for: temperament, judgment under pressure, willingness to disagree professionally, awareness that the technical decision is rarely the only decision in the room. ## The reference call that matters more than the interview If a candidate gets through all four rounds, you talk to references. Not the references they list on the resume — those are filtered. Talk to the people who reported to them, and to the people they reported to. Three questions, in this order: - "What's the kind of work this person is best at?" — Calibrates strengths. - "What kind of work do they struggle with?" — Calibrates limits. If the reference can't name a single weakness, they didn't know the candidate well enough; the call is useless. - "Would you hire them again?" — The most predictive single question in any reference check. The pause, the tone, the qualifications they put on the answer matter more than the literal yes or no. Three reference calls is enough. Five is overkill. One is too few. ## AI-fluency calibration: the new bar Here's where the AI-startup version of staff hiring diverges from generic staff hiring. In 2026, an engineer who can't fluently use Claude Code, Copilot, or Codex is not a staff engineer. They might be a great senior engineer, but they're not operating at the leverage a staff engineer should have. The bar I'd hold to: - **They use AI tooling daily.** Not "I've tried Copilot." Not "I'm a skeptic." Daily, with opinions about which tool for which job. - **They know what NOT to use AI for.** Auth code, real-money flows, performance-critical paths. If a candidate says "I use AI for everything," that's a flag. - **They have a take on team adoption.** A staff engineer should have thought about how AI tooling changes engineering practice at the team level, not just personal productivity. How to test it: a question in round 4. "Walk me through how you used AI in the last feature you shipped." Listen for specificity. Vague answers signal lip-service usage. Specific answers — "I used Claude to enumerate the edge cases on the state machine before I wrote the code, then I had it review my PR before I opened it" — signal real fluency. If a candidate is otherwise excellent but lacks AI fluency, hire them anyway and budget two months for them to develop it. If they're average plus AI-skeptical, decline. The bar has moved. ## Comp, equity, and selling them A staff engineer at an AI startup pre-Series-A in 2026 is looking at: - **Cash:** $220–280K base. Bay Area / NYC / remote-but-competitive. - **Equity:** 0.4–0.8% over four years. Higher end if they're early; lower end if they're hire 8. - **Sign-on:** $25–50K to make up for unvested equity at their previous company. The candidates you want at this level have options. Selling them is half the job. The pitch that lands: a clear, articulate vision of what the engineering org is going to look like in 18 months, what their role in shaping it is, who they'll be working alongside, and what the realistic path to Series A and beyond looks like for the company. The pitch that doesn't land: "we're hiring fast, lots to do, hope you like ambiguity." That's not a pitch, that's a confession. ## The first 30 days after they say yes Hiring a staff engineer is half the work. The other half is onboarding them so they're operational at the level you hired them for. Three commitments to make in writing during the offer stage: - A clear "first 90 days" set of expectations. What good looks like at day 30, day 60, day 90. - Direct access to the founder for the first 30 days. Weekly 1-on-1s. They are part of how the company is run, not three layers below it. - Ownership of one tangible piece of the platform within 30 days. Not "shadowing." Not "learning the codebase." Something they own with their name on the GitHub commits. Without these three, the staff engineer ramp drifts and they end up doing senior-engineer-level work for two quarters before someone notices. With them, they're contributing at staff level by month two, and the existing team is leveling up against them. ## The takeaway Hiring a staff engineer is one of the most consequential hires a pre-Series-A AI startup makes. Get it right and the entire engineering bench levels up. Get it wrong and you've spent $300K+ all-in on a hire who either underperforms or leaves. The interview process I've described is more rigorous than what most companies run. That's the point. The cost of running this process is high. The cost of hiring the wrong staff engineer is higher. If you're a founder who hasn't run this kind of loop before, partner with someone who has. The first staff hire is not the time to learn the process from scratch. ## Read this next - [**The Pre-Series-A AI Startup Hiring Plan**](https://sublimecoding.com/blog/pre-series-a-ai-startup-hiring-plan) — Where the staff engineer fits in the broader hiring sequence. - [**From One Engineer to Fifteen**](https://sublimecoding.com/blog/from-one-engineer-to-fifteen-engineering-leadership) — The leadership lessons that inform how I'd onboard a staff hire today. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — The AI-fluency bar, in much more detail. --- ## The Perfect Hire Is Killing Your Team URL: https://sublimecoding.com/blog/perfect-hire-killing-your-team Published: 2026-06-26 Tags: hiring, engineering leadership, founders > **TL;DR:** Most engineering teams hire for the wrong things. They screen for pedigree and the ability to perform algorithms on a whiteboard under a running clock — two of the *weakest* known predictors of who actually does good work. They reject the people who’d have compounded into their best engineers: the ones who put in the effort, own the outcome, and keep growing. The whiteboard ritual measures a skill nobody uses on the job, the “perfect match” you’re hunting is a fiction, and the search for it is what’s hollowing out your team. Stop hiring for the intercept. Hire for the slope. ## The interview tests a skill nobody uses at work Picture the standard loop. Reverse a linked list on a whiteboard. Implement quicksort from memory. Explain the time complexity of an algorithm you last touched in a CS class, with no editor, no documentation, no internet, and a stranger watching the clock. Get it perfect on the first try or lose points. Now picture the actual job. Nobody writes a sorting algorithm from scratch — they call the one in the standard library, because re-implementing well-documented, battle-tested code by hand is how you introduce bugs, not how you ship. The real skill on the job is knowing *which* tool to reach for, reading the docs well, composing existing pieces, and recognizing when the obvious approach is wrong. The interview measures the opposite of that: recall under artificial pressure, of things you would and should look up the moment you were doing the work for real. We built a hiring ritual around a performance that has almost nothing to do with the performance we’re actually buying. ## What the whiteboard actually measures — and what the data says It measures two things: how much syntax and trivia you’ve memorized, and how calmly you perform while being judged. Neither is the job. This isn’t a hot take; it’s what the people with the most hiring data concluded years ago. Google ran the numbers on its own famously brutal process and [Laszlo Bock, its head of People Operations, called brainteasers “a complete waste of time”](https://www.inc.com/adam-robinson/google-used-this-brainteaser-to-identify-top-job-candidates-heres-why-it-failed.html) — they predicted nothing except a candidate’s ability to solve brainteasers, and mostly served to make the interviewer feel clever. Google replaced them with structured, work-sample-style assessment because that’s what actually correlated with performance. The broader selection-science research says the same thing, and it’s held up even after a [2022 reanalysis that corrected decades-old validity estimates](https://www.siop.org/tip-article/is-cognitive-ability-the-best-predictor-of-job-performance-new-research-says-its-time-to-think-again/): work-sample tests and structured interviews sit near the top of the predictive-power ranking, while **years of experience, GPA, and educational pedigree sit near the bottom.** Read that again, because it’s the whole game. The two signals the typical broken interview leans on hardest — an impressive résumé and a flawless on-the-spot puzzle solve — are among the *least* predictive of whether someone will be good at the job. You are optimizing your filter for noise. ## “I don’t know — I’ll look it up” is a senior answer Here’s a thing the best engineers I know have in common: they don’t have the docs memorized, and they’re completely unbothered by that. They know where the materials live, they know how to evaluate what they find, and they know how to apply it. Ask them something outside their working memory and they’ll say, plainly, *“I don’t know that off the top of my head — I’d look it up and figure it out.”* In most interview formats, that honest, accurate, senior answer loses points. We’ve built a process that rewards the candidate who confidently recites and penalizes the one who tells the truth about how knowledge actually works. But there are always gaps. There are always limits to what any one person carries in their head. Pretending otherwise — treating recall as competence — selects for confident memorizers over honest problem-solvers, which is exactly backwards. And in 2026 this is no longer even debatable. The memorization premium has collapsed. [AI-assisted engineering isn’t faster typing — it’s a different workflow](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow), one where the durable skill is knowing what to ask, how to verify the answer, and how to integrate it safely. The engineer who reaches for the right tool and validates the output is demonstrating the *actual* job. The whiteboard interview was an outdated philosophy long before LLMs arrived; the tools just made it impossible to keep pretending otherwise. ## Everyone knows this, and we keep doing it anyway I have yet to meet an engineer who loves the algorithmic-whiteboard gauntlet and thinks it’s a wonderful way to find talent. The only people who enjoy it tend to be the ones who enjoy competitive programming and grinding challenge sites for their own sake — a real and fine hobby, and also not the same thing as the job. So why does it survive? Because it’s easy to administer, it *feels* rigorous and objective, it lets the interviewer feel smart, and it’s what the big-name companies do, so copying it feels safe. None of those reasons is “it works.” It’s cargo-cult hiring: imitating the visible ritual of a process whose actual results you never measured. The cost is that you filter your entire pipeline down to one narrow profile — the person who recently drilled LeetCode — and quietly discard everyone else. ## Hire for the slope, not the intercept Here’s the reframe. Stop trying to measure where a candidate is *today* with a trivia exam, and start trying to measure which direction they’re moving and how fast. Hire for the slope. The traits that compound are effort, ownership, and coachability. The willingness to put in the work, the time, and the deliberate practice to keep getting better is worth more over two years than any amount of memorized syntax — because the memorized syntax is a depreciating asset and the growth habit is an appreciating one. The trait I’d weight highest is the one I’ve written a whole essay about: [owning the whole outcome](https://sublimecoding.com/blog/professional-owns-the-outcome), not just the assigned task. That predicts more than any credential. And the part most hiring managers miss entirely: **good teammates are forged, not found.** A large fraction of how well someone performs is a function of the team around them — the standards they’re held to, the review they get, the patterns they absorb. [The rituals that make a small team good](https://sublimecoding.com/blog/managing-a-four-person-engineering-team) are also what turn a promising hire into a great engineer. If you’re only willing to hire someone who’s already perfect, you’ve outsourced your own most important job: building the environment that makes people better. The juniors you skip because they “aren’t there yet” are precisely the [engineers you’ll wish you’d grown](https://sublimecoding.com/blog/stop-making-senior-engineers) two years from now. This is why the unicorn hunt is so corrosive. The candidate who checks every box, matches every keyword, and clears the whiteboard flawlessly is mostly a fiction — and chasing that fiction leaves seats empty for months, homogenizes your team into one profile, and rejects the people who would have become your strongest contributors. The fix isn’t to stop assessing skill. It’s to assess the *real* skill: give candidates a realistic task close to the actual work, let them use their tools and their references the way they would on the job, and watch how they think, how they recover from not knowing, and how they own the result. That’s the constructive version — [the actual process I run](https://sublimecoding.com/blog/how-id-hire-a-staff-engineer-at-an-ai-startup) — and it has nothing to do with reversing a linked list under fluorescent lights. The question was never whether someone can recite an algorithm on command. It’s whether they’ll own the outcome, do the work, and keep growing. Stop searching for the perfect match. Build the team that forges great engineers — and hire the people who want to be forged. --- ## The Pre-Series-A AI Startup Hiring Plan: Who to Hire, in What Order, and Why Most Get It Wrong URL: https://sublimecoding.com/blog/pre-series-a-ai-startup-hiring-plan Published: 2025-12-29 Tags: hiring, founders, AI startups, engineering leadership, business **Most pre-Series-A AI founders hire in panic order, not strategic order. The result is a team that can't ship the product the company actually needs to build.** **Key takeaways** - **Hire in strategic order, not panic order.** Founding engineer → second backend generalist → first frontend specialist → infra/platform → first PM → second backend cluster → first eng manager around hire 8–10. - **Don't hire a Director of Engineering before there are at least two ICs to manage.** Don't hire a CISO before Series A. Don't hire a Head of AI when an IC with a clear mandate works. - **Equity-heavy through hire 5; base-heavy after.** Comp calibrated to stage and risk, not to what FAANG would pay. The pattern I see, repeatedly: a founder closes a seed round, gets pressure from the board to "scale the team," and posts five senior backend engineering openings on a Monday morning. Six months later they've hired four backend engineers, the product still doesn't have a designer, the AI features they're shipping look like internal tools, and the BD pipeline that was supposed to fund the next round is empty because no one has been working it. The right framing is not "scale the team." It's **each hire should either unblock the product or unblock the customer**. If the hire doesn't do one of those, it's an expensive bet you didn't need to make at this stage. Here's the plan I'd run if I were starting an AI-native company today and going from two co-founders through to a Series A. ## The six hires before Series A For an AI-native company with two technical co-founders raising a $3–5M seed round, this is the order I'd hire in. The total span is roughly 14 to 18 months from first close to Series A. ### Hire 1: Founding engineer The first hire is not a "great engineer." The first hire is a third co-founder who didn't get the title. What you're looking for: full-stack capability, willingness to own a feature end to end, the temperamental capacity to be the only one in the codebase besides you for the first six months. Someone who's been a senior IC at one or two real companies and has decided they want startup risk now. Comp: heavy equity (1.0–2.5%), market-rate-or-below cash. If they're asking for FAANG cash plus founding-engineer equity, they're not the right hire. The math doesn't work and they're going to bail at month 9. What this hire should NOT be: a specialist. The first hire is the second pair of hands across the entire stack. The specialists come later. ### Hire 2: Product designer The single most counter-intuitive hire on this list is also the most important. AI products that look like engineering tools die. Almost without exception. Your customers cannot tell whether your model is good. They *can* tell whether your product feels considered. A great designer in seat from month four will reshape every feature you ship — for the better — and meaningfully change what an enterprise prospect sees in your demo. What you're looking for: someone who's shipped product design at a venture-backed startup, ideally one with a complex underlying technology. Senior level. Comfortable with no full-time PM in seat (you're the PM, the founder, until much later). Comp: market-rate cash, 0.5–1.0% equity. Fewer designers than engineers in the candidate pool, so you'll pay closer to senior-PM rates. ### Hire 3: ML or Applied AI specialist By month six or seven, your AI features have moved past "wrap an LLM in a UI" and into territory where someone needs to think hard about prompt engineering, retrieval, fine-tuning, evals, and the rest of the AI engineering stack. This is not the founding engineer's job. This is a specialist. What you're looking for: someone who's shipped AI features in production at another company. *Not* a research scientist. Not a PhD straight out of grad school. The hire is "applied" — they know how to ship, they know how to handle the messiness of LLMs in production, and they have opinions about evals. Comp: market-rate cash, 0.4–0.8% equity. Hot market — be ready to move quickly when you find the right one. ### Hire 4: The GTM hire This is where most founders get the order wrong. They hire engineer 3, then engineer 4, then engineer 5, then somewhere around month twelve realize they have no one running the customer side and they're still doing all the BD calls themselves. By the time you're at four engineers, you should have one person whose job is owning customer development end to end. What flavor of GTM hire depends on your product: - **Founder-led sales motion still working?** Hire a founding BDR / sales associate to handle the top of funnel and let the founder close. - **Self-serve / PLG product?** Hire a growth engineer who's also done marketing. - **Enterprise contracts already pulling?** Hire a founding AE — yes, even at $200k base + variable + equity. The math works if they close one deal. This hire pays back the seed round in pipeline within their first year if you've hired the right person. Skipping it for "one more engineer" is the most common pre-A mistake. ### Hire 5: Senior product engineer Now, finally, you hire engineer #3 (after the founding engineer and the AI specialist). This is the engineer who builds product features against the backlog the designer has shaped. What you're looking for: someone who's shipped product features at scale at a previous startup. Less senior than the founding engineer, but with enough taste to make the right tradeoffs without supervision. Strong frontend or strong full-stack — depends on where the gap is at this point. Comp: market-rate cash, 0.3–0.5% equity. ### Hire 6: Security / ops person By month 14, your customer pipeline is asking for SOC 2, vendor questionnaires, and a security trust page. Your [vCISO](https://sublimecoding.com/tools/vciso-cost) has been doing the strategy work, but you need someone in seat for the day-to-day execution. This hire is part security engineer, part DevOps, part compliance ops. What you're looking for: someone with cloud security and compliance ops experience at a startup of similar stage. Not a full CISO yet — you're not ready for that role. Senior IC with leadership trajectory. Comp: market-rate cash, 0.3–0.5% equity. The vCISO transitions to advisor; the in-house person owns execution. ## The hires NOT to make pre-A For every hire on the list above, there's a tempting wrong-stage hire that founders make instead. The list of *don'ts*: - **Don't hire a full-time PM yet.** Founder is PM. The day you hire a PM is the day product velocity drops 30% as the PM "gets up to speed" and adds a layer between engineering and customers. Wait until post-A. - **Don't hire an EM yet.** Same reason. You're managing six engineers; you don't need an engineering manager. The founding engineer is the de-facto tech lead. - **Don't hire a CISO.** Hire a vCISO (covered in [vCISO Math](https://sublimecoding.com/blog/vciso-math-for-ai-founders)). Save the full-time hire for $20M ARR or after a regulatory event. - **Don't hire a research scientist.** Almost all AI startups don't need one. The applied AI specialist (hire 3) is sufficient until you're shipping novel research as the product. - **Don't hire a full-time recruiter.** Founder is recruiter. If you can't recruit your own first six hires, you don't yet know what you're hiring for. - **Don't hire a head of marketing.** Wait until you have a repeatable GTM motion the head of marketing can scale. Until then, the founder owns positioning. ## The compensation framework The biggest reason founders blow this plan is bad comp framework. They either underpay and lose candidates to bigger checks, or overpay and burn the runway they need for the next 18 months of progress. The framework that's worked for me: - **Cash:** roughly 80–90% of market median for a senior at a similar-stage startup. Pull market data from Carta, Pave, or Levels.fyi. Pay slightly below median because you're paying in equity. - **Equity:** heavy for early hires (founding engineer 1.0–2.5%), tapering down (hire 6 at 0.3–0.5%). Use a tool like Carta to manage option pool dilution carefully. - **Refresh grants:** commit in writing to a refresh grant at the 24-month mark. This is how you keep early hires from leaving when their original grant gets eclipsed by new joiners' grants. - **Cash-vs-equity flexibility:** some great candidates need more cash because of life circumstances. Have a documented sliding scale (e.g., "+$20k base = -0.2% equity") so you're not negotiating each one from scratch. ## The post-Series-A inflection This plan stops at six hires. After Series A, the discipline changes. You'll go from six to roughly thirty in the year following the A. The hire-by-hire framing breaks down at that velocity; you start hiring against role profiles and team needs. The right framing at that scale is "how many engineers do we need to ship the product roadmap" — but you only earn the right to ask that question after you've shipped pre-A with a tight team that proves the product works. The single biggest predictor of which AI startups successfully transition pre-A to post-A is whether the team they assembled before the Series A could actually ship. The roster matters more than the headcount. Get the first six right and the rest of the company is downstream of that decision. ## What it actually costs to get this wrong Founders skip past hiring sequencing because the cost of getting it wrong feels abstract. It isn't. Here's what hiring two extra engineers in months 4–6 instead of a designer + a GTM hire actually costs. - **Two engineers fully loaded:** ~$500K cash + 1.0% equity over 18 months. - **Lost product quality from no designer:** hard to quantify directly, but typically manifests as enterprise demos that don't convert. Three lost enterprise deals at $80K ACV each = $240K in lost first-year revenue. - **Lost pipeline from no GTM hire:** a competent founding BDR generates $300–500K in qualified pipeline in their first six months. Not having one means the founder is doing top-of-funnel work instead of product or fundraising. - **Compounding delay:** the Series A pitch eighteen months later is "we have great product, weak distribution" — a much harder pitch than "we have great product and a working GTM motion." Down-round risk goes up materially. Total expected cost of the wrong sequencing in real dollars and equity: somewhere between $800K and $1.5M over two years, plus the fundraising delta. The right sequencing has a better expected value *even if the product takes one more month to ship*, because the customer-side work compounds in parallel with the engineering work. ## Read this next - [**How I'd Hire a Staff Engineer at an AI Startup**](https://sublimecoding.com/blog/how-id-hire-a-staff-engineer-at-an-ai-startup) — A deep dive on the screen, take-home, and interview loop for one of the most consequential roles on this list. - [**From One Engineer to Fifteen**](https://sublimecoding.com/blog/from-one-engineer-to-fifteen-engineering-leadership) — What I learned scaling an engineering team during my own founding stretch. - [**vCISO Math for AI Founders**](https://sublimecoding.com/blog/vciso-math-for-ai-founders) — Why hire #6 starts as a vCISO, not a full-time CISO. - [**How to Manage a 4-Person Engineering Team Without Becoming a Manager**](https://sublimecoding.com/blog/managing-a-four-person-engineering-team) — The 5 rituals that work at 3–5 engineers, before you need real process. --- ## The Ruby to Elixir Migration That Cut Our Service Footprint From Ten to Six URL: https://sublimecoding.com/blog/ruby-to-elixir-migration-ten-to-six-services Published: 2026-02-09 Tags: Elixir, Ruby, Phoenix, migration, engineering, OTP **We had ten microservices that were 60% Ruby and 40% Elixir. Two years later we had six, fully Elixir, and our on-call alert volume had halved.** The migration was less about the language and more about what running real-time messaging for 450,000 active students across 900 partner universities forced us to think about. Memory pressure. Long-running connections. Concurrency that didn't tip over. Operational ergonomics that made on-call survivable. Ruby could do all of these things, but every solution required a layer of accidental complexity that Elixir's runtime gave us for free. What follows is the migration playbook from InsideTrack — the order that worked, the patterns we leaned on, the unexpected wins, and the parts I'd do differently with what I know now. ## The stack we started with The platform served two-way messaging between coaches and students. Mostly SMS, some email, increasing volume of in-app chat. The architecture, when I joined: - Three Rails monolith services (web, API, admin) - Two Sidekiq workers (one for messaging dispatch, one for analytics ingestion) - Three small Sinatra services (one webhook receiver, one cron scheduler, one feature-flag service) - Two early Phoenix services (a real-time inbox and a notification dispatcher) — both written by the previous team in a "let's try Elixir" experiment Total: 10 services, 6 Ruby, 4 Elixir. Combined the team operated 60+ background workers and a Postgres cluster handling several thousand writes per second at peak. The motivation to consolidate wasn't ideological. It was operational. The Ruby services were memory-hungry, the Sidekiq workers had to be horizontally scaled aggressively to keep up with peak load, and the on-call rotation was getting paged 8–12 times per night during exam season because of the cumulative weight of running too many services. ## The trigger to start moving Two specific events forced the decision. First, we lost a contract with a large university because our messaging dispatch latency P99 spiked above the contractual threshold during an exam-season peak. The latency wasn't a code bug — it was Sidekiq queue depth backing up because the worker fleet couldn't scale fast enough. We could have thrown more Sidekiq workers at it, but the marginal cost was high enough that we'd have eaten the contract margin. Second, our on-call engineer quit. The exit interview was honest: too many services, too much ambient alert noise, no clear ownership boundaries. The team morale knock was as expensive as the lost contract. The combined message — both customer-facing and internal — was that the architecture was the bottleneck. Not the team's effort, not their skill, not the underlying tech of any single service. The number of services was the problem, and the runtime characteristics of Ruby + Sidekiq made consolidation in Ruby genuinely hard. Elixir's BEAM gave us a runtime that handled the same workload with one or two services instead of seven. ## What's the right order to migrate Ruby services to Elixir? The first lesson I learned was that migrations work backwards. You don't migrate the easy thing first; you migrate the thing that's most painful to keep on the old stack. Our order, in retrospect: - **The messaging dispatcher.** The most painful service. The one driving the on-call alerts. Migrating it first meant on-call ergonomics improved within the first quarter and the team had visceral evidence the migration was paying off. - **The analytics ingestion worker.** Second-most painful. Sidekiq queue depth here was a chronic capacity issue. Re-implementing as a GenStage pipeline in Elixir collapsed memory usage by ~70%. - **The webhook receiver and cron scheduler.** Smaller services we consolidated into a single Phoenix app with multiple endpoints and a Quantum scheduler. Saved two services in one move. - **The feature-flag service.** Replaced wholesale with a managed service (LaunchDarkly). Not strictly an Elixir migration — but the Ruby-to-Elixir framing forced us to evaluate "is this our problem to host at all?" and the answer was no. - **The Rails admin service.** Migrated to Phoenix LiveView. Surprised us by being one of the easier moves once we got over the learning curve. - **The Rails API service.** Migrated last and most carefully. This was the customer-facing surface; we ran a dual-deploy period for two months with traffic mirrored to both stacks for parity testing. - **The Rails web monolith.** Stayed Ruby. We never migrated it. Too much business logic, too low a marginal benefit. Lesson: not everything needs to move. Final state: six services, all Elixir except the Rails web monolith. One major Phoenix app handling messaging dispatch, ingestion, webhooks, scheduling, and admin. Three smaller Phoenix apps for the inbox, notifications, and a public API. Plus the Rails web monolith. Down from ten. ## The wrong order I tried first My initial plan, before reality course-corrected it, was to start with the API service. Reasoning: it's the most visible, it has the most code, getting it migrated first proves the platform. That plan was wrong. The API service was the riskiest single move and had the lowest operational pain associated with it. We would have spent six months on a high-risk migration that wouldn't have meaningfully reduced on-call burden, while the messaging dispatcher kept paging us. The team would have lost faith in the migration before we got to the actually painful services. The corrected ordering — pain first, value-prove second, polish last — is the framework I'd use again. **Migrate the service that's hurting you most, even if it's not the most strategic one.** The early operational win pays for the political capital you'll spend later on the harder migrations. ## The Elixir patterns we leaned on Three OTP primitives did the bulk of the work. **GenServer for stateful work.** The messaging dispatcher's previous architecture was Sidekiq + Postgres rows for state. Re-implementing as GenServers per active conversation eliminated the database churn for state machine transitions and let us hold conversation state in memory cheaply. The supervision tree handled crashes per-conversation without taking down the whole dispatcher. **Registry for routing.** Looking up "which GenServer handles conversation 42" is a few microseconds with Registry. We used it everywhere — for active conversations, for active user sessions, for active webhook subscriptions. Dead simple, fast, and it eliminated a class of "where does this message go" problems that had been complex in the Ruby version. **Supervision trees for failure isolation.** The single most important property of the Elixir runtime is that one bad message can't take down the service. A ten-thousand-conversation dispatcher might have one or two crashing GenServers at any given moment; they get restarted in milliseconds and the other 9,998 conversations don't notice. Sidekiq could not give us this without significant infrastructure investment. The fourth pattern, less universal but useful: **GenStage for backpressure-aware pipelines.** The analytics ingestion worker was a GenStage pipeline with explicit demand-driven flow control. Made the queue-depth-spike pattern that had been killing us in Sidekiq simply not exist as a category. ## The unexpected wins **Halved on-call alerts.** By far the biggest morale and retention win. The team that had been getting paged 8–12 times a night dropped to 3–4. Not because the services were doing less work, but because they handled load shedding, partial failures, and self-healing without paging humans. **Better dev ergonomics for the kind of work we did.** Pattern matching against incoming messages made the dispatcher code dramatically clearer than the Ruby case statements it replaced. `iex` with remote shell into a running production node was an operational superpower. **Hiring quality went up.** This surprised me. The Elixir candidate pool is smaller, but the candidates who self-select into Elixir tend to be more curious and more rigorous than the Ruby candidate average. We hired better engineers per interview hour after the migration than before. ## The unexpected losses **Gem ecosystem.** I missed Devise. I missed ActiveAdmin. I missed Sidekiq Pro's UI. There were Elixir-equivalent libraries for most of these, but the Elixir ecosystem in 2018-2019 was visibly less mature, and rolling our own auth or admin UI cost more time than the migration math accounted for. **Hiring pool narrower.** Yes, the candidates who came through were better. But the funnel was smaller. We'd see 30 Ruby applicants for every 5 Elixir applicants. For a small team this didn't matter. For a team scaling fast, it would have been a constraint. **Internal training cost.** Engineers coming from Ruby need 2–3 months to be productive in Elixir. We absorbed that cost but it was real and it slowed the migration. Account for it explicitly in your timeline. ## When should you not migrate to Elixir? The math has shifted somewhat since 2019. I would not unconditionally recommend a Ruby-to-Elixir migration today. The cases where I'd push back: - **You're not running real-time, long-lived connections.** The killer features of the BEAM are concurrency and supervision. If your workload is short, request-response, and stateless, Ruby/Rails on a modern hosting platform is genuinely fine. - **Your team has zero Elixir experience and you're already understaffed.** The 2-3 month productivity dip per engineer is real. If you can't afford it, don't start. - **Your product is dominated by AI features, not real-time messaging.** The AI ecosystem in Python is significantly stronger than in Elixir. Most AI startups today should be in Python or Go for the AI portion, regardless of what the rest of the stack runs. - **Ruby 3 + YJIT is meeting your needs.** The performance gap between modern Ruby and Elixir narrowed considerably with YJIT. If your Ruby services aren't hurting you, leave them alone. The right reason to migrate is operational pain that's expensive to solve in your current runtime. The wrong reason is novelty. ## What I'd do differently If I were running this migration again today: - **I'd budget the per-engineer onboarding cost explicitly.** 60 days off the keyboard for the first migration project, then ramp. We crashed into this; it should have been planned. - **I'd build the dual-stack observability layer first.** Migrating with consistent metrics across both stacks would have made the parity testing meaningfully easier. We bolted this on. - **I'd skip the LiveView migration of admin and use a managed admin tool.** LiveView is great. The admin we built was fine. But the time we spent on it was better spent on the API migration. - **I'd not migrate the Rails web monolith. Same conclusion. We made the right call there.** If you're scoping a migration like this — language consolidation, service-count reduction, or a Ruby/Rails monolith that's hit its limits — I do this kind of architectural work as a [fractional engineering lead](https://sublimecoding.com/consulting). The interesting decisions aren't language choice; they're sequencing and what you preserve in the existing data layer. ## The takeaway Migrations are paid for by operational pain reduction, not by language preferences. The Ruby-to-Elixir move at InsideTrack worked because real-time messaging is exactly the workload BEAM is built for, and the operational pain we were running into was specifically the kind that BEAM eliminates. For other workloads, the calculation may go the other way. The disciplined version of the question — "what's hurting us today, would moving runtimes solve it cheaply, and can we afford the transition cost" — is a much better framing than "what's the right tech stack for our company in 2026." The right answer to that latter question is almost always "the one you already have, optimized harder." ## Read this next - [**Migrating 225K Users from AWS Cognito to Auth0**](https://sublimecoding.com/blog/aws-cognito-to-auth0-migration-without-forcing-logout) — A different migration war story, same disciplined pattern: pain first, value-prove second. - [**How We Cut $350K From Cloud Spend**](https://sublimecoding.com/blog/cut-350k-cloud-spend-six-months) — When the platform you migrate to also rewrites the cost structure. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — How modern teams approach migrations with AI tooling in the loop. --- ## Your AI Product Needs a Telemetry Layer Before It Needs a Better Model URL: https://sublimecoding.com/blog/your-ai-product-needs-telemetry-before-better-model Published: 2026-01-26 Tags: AI, engineering **I've watched three AI startups burn months trying to "improve the model" when they couldn't even tell which prompts produced which outputs at scale.** Every team had the same instinct: hallucination rate too high, response quality inconsistent, costs creeping — must be a model problem, let's tune the prompts, let's swap to GPT-5, let's fine-tune. None of them stopped to ask the more useful question first: *what's actually happening inside the model calls we're already making?* The answer, almost always: nobody really knew. There was no production logging of prompts. No structured capture of model outputs. No correlation between which user did what and what the model returned. The team was making decisions about model improvement based on cherry-picked screenshots and vibes. That's not a model problem. That's an instrumentation problem. And it's solvable in two weeks of disciplined engineering, which buys you the visibility to know whether the model problem is even real. ## What AI telemetry actually means Classic application telemetry — request rate, latency, error rate — does not tell you anything useful about an AI feature. A successful 200 response from your LLM endpoint tells you nothing about whether the response was correct, helpful, or hallucinated. You need a different layer of observability that's specific to how AI features fail. The four things you must capture for every model call: - **The full prompt.** Every variable interpolation. Every system prompt. Every retrieval-augmented context. Stored as structured data, not a stringified blob. - **The full response.** Including any tool calls, function calls, or structured outputs. Stored verbatim. - **The cost and latency.** Tokens in, tokens out, dollar cost, wall-clock time. These compose into your unit economics. - **The user context.** Who triggered this call, in what feature, against what state. Anonymized if you must, but linkable to the user session. Without those four, you cannot reason about model performance at any scale beyond "let me copy this prompt into the playground and see what happens." That's not engineering, it's gambling. ## The four-layer telemetry stack Once the basics are captured, the actual decisions you make benefit from layered aggregation. ### Layer 1: Request-level telemetry Every model call gets logged with the four-tuple above plus a request ID. This is the source of truth. Every other layer aggregates from this layer. Storage decisions matter here. The volume can be large — for a product making 100k model calls a day, this is 100k structured rows daily. We chose Postgres with JSONB columns at Lavender, with a 90-day retention policy. Worked fine for our scale; would not scale to 10M calls/day. Use what fits. ### Layer 2: Feature-level aggregation Each model call belongs to a feature: "summarize," "draft email," "suggest reply," etc. Aggregate the request-level data by feature to answer questions like: - What's the median response time of the "draft email" feature this week? - What's the daily cost of "summarize" over the past 30 days? - Which feature has seen the biggest cost spike since the last release? This is the layer where you start making product decisions: "the suggest-reply feature costs 4x what summarize does and gets used 1/10 as much — we should kill it or rebuild it." ### Layer 3: User-level signal Each user has interactions across multiple features. Aggregate at the user level to answer: - Are heavy users seeing more or fewer hallucinations than light users? - Is there a cohort of users for whom the feature consistently fails? - What's our cost per active user per week? The user-level layer is where you discover that your model is fine for 90% of users but catastrophically bad for the specific use case 10% of users have. Without this layer, that 10% is invisible. ### Layer 4: Aggregate trends and regression detection Daily / weekly rollups across the whole product. The metrics that go on a dashboard the founder reads every Monday morning: - Total cost trend - Cost per active user trend - P95 latency trend - Hallucination signal trend (more on this below) - Feature-level usage distribution The point of layer 4 is regression detection. When something breaks, you want to know within 24 hours, not 21 days into the quarter when finance asks why the OpenAI bill tripled. ## The hallucination signal Hallucination is the hardest thing to measure because there's no ground truth label at runtime. Real-world signals that approximate it: - **User regenerates the response.** One of the strongest negative signals. If a user immediately clicks "regenerate," they didn't like what they got. - **User edits the response heavily before using it.** If you have a copy-and-edit flow, measure the edit distance. - **User abandons the feature mid-flow.** Strong signal something went wrong. - **Explicit thumbs-up / thumbs-down.** Lowest-volume signal but the cleanest. Add it everywhere it's not annoying. - **Response contains markers of uncertainty.** "I don't have information about" or "I cannot determine" — sometimes useful, sometimes a euphemism for hallucination. None of these is a clean ground-truth label. Combined, they give you a directional indicator that's good enough for relative comparisons over time. The goal isn't "what's our true hallucination rate" — that's unanswerable. The goal is "is hallucination getting better or worse this week, and which features are driving the change." ## Tooling I'd reach for The build-vs-buy decision for AI telemetry has shifted in the last 18 months. There are now real options. - **[LangSmith](https://www.langchain.com/langsmith)** — strong if you're already using LangChain. Decent if you're not. Captures request/response/cost out of the box. - **[Helicone](https://www.helicone.ai/)** — proxy-based capture. Lowest integration cost — point your LLM SDK at Helicone's URL, get telemetry for free. Best for early-stage teams that want zero-config. - **[Langfuse](https://langfuse.com/)** — open source, self-hostable. Good for teams with security/data residency concerns. - **Custom OpenTelemetry instrumentation.** If you already have a strong observability stack (Datadog, Honeycomb, etc.), wrapping your model calls in OTel spans is sometimes the right answer because it integrates with existing dashboards. For pre-Series-A AI startups I'd start with Helicone and graduate later. The integration cost is one afternoon. The telemetry you get back is enough to make the next dozen product decisions correctly. ## Model problem or instrumentation problem? The most useful framing I've found, when an AI feature is underperforming: **Can you, right now, answer these five questions in under five minutes?** - What was the prompt and response of the last 10 calls to this feature? - What's the median latency for this feature over the past 7 days? - What's the daily cost for this feature, broken out by model? - Which users had the worst experiences this week, by hallucination signal? - How does any of this compare to two weeks ago? If the answer to any of these is "I don't know" or "let me write a query," you have an instrumentation problem, not a model problem. Fix instrumentation first. Then look at the data, and the model problem either becomes obvious — or evaporates because what looked like a model problem was actually a prompt regression in last week's deploy. ## A concrete example At Lavender, we shipped a new prompt template for one of our AI features early in 2025. The hallucination signal — measured via the regenerate-rate — climbed about 60% over the next two weeks. The instinct was "the new prompt is worse, let's rewrite it." Telemetry told a different story. The regenerate-rate climbed for users on a specific email template that one of our customer-success team had recommended internally. The new prompt was fine. The customer template was triggering an edge case we hadn't anticipated, and the regenerate-rate spike was an artifact of that template being used 4x more than usual. The fix was a 20-line guardrail in the prompt that handled the edge case. Hallucination signal dropped by 40% within 72 hours. We didn't tune the model. We didn't change LLMs. We did not run a single eval. We instrumented, looked at the data, found the actual cause, fixed it. That story is impossible to tell without telemetry. Without it, the team would have spent two weeks rewriting the prompt, regressing on something else, and ending up worse than where they started. With it, the cause was obvious within 90 minutes of looking at the data. ## The takeaway Most AI startups will eventually need to think hard about the model. None of them should think about the model first. The order is: - **Instrument.** Capture every model call, structured and queryable. - **Aggregate.** Build feature-, user-, and trend-level views. - **Look.** Stare at the data for a week. Most "model problems" reveal themselves as something else. - **Then, if needed, tune the model.** But you'll be tuning against actual data, not vibes. The two weeks of disciplined engineering this requires is the highest-leverage AI work most startups aren't doing. It's also boring. Which is exactly why doing it is an edge over teams that go straight to fine-tuning. ## The team discipline this requires Telemetry is a code problem for half a sprint and an organizational problem forever after. The engineering team has to keep instrumentation current as new features ship, or the system rots within a quarter. The disciplines that worked at Lavender: - **No model call ships without telemetry.** Code review checklist item, enforced. New AI feature PRs get rejected if they don't wire up the four-tuple capture. - **One engineer owns the telemetry layer.** Not full-time, but they're the named point of contact. Schema evolution, dashboard updates, retention policies — they own it. Without an owner, the layer drifts. - **Weekly review of the dashboards.** 15 minutes at the top of an engineering meeting. Just looking at the trends. Catches regressions while they're small and trains the team to think in terms of these metrics. - **Cost alerts before user complaints.** If the daily AI spend deviates from the rolling 7-day median by more than 30%, it pages the on-call. Most product issues show up here before they show up in support tickets. The instrumentation work is one or two weeks. The discipline of keeping it useful is forever. Build the muscle early — adding it later, against an existing AI product with no telemetry, is meaningfully harder. ## Read this next - [**How I'd Run Security at an AI-Native Company in 2026**](https://sublimecoding.com/blog/running-security-at-an-ai-native-company-2026) — The audit-and-security layer of AI observability — what to log for incident response and customer trust. - [**AI-Assisted Engineering Isn't Faster Coding**](https://sublimecoding.com/blog/ai-assisted-engineering-is-a-new-workflow) — Engineering discipline applied to the team using AI; this post applies it to the AI itself. - [**How We Cut $350K From Cloud Spend**](https://sublimecoding.com/blog/cut-350k-cloud-spend-six-months) — The same instrumentation discipline applied to infrastructure, with bigger dollar consequences. --- ## Surviving Technical Due Diligence as an AI Founder URL: https://sublimecoding.com/blog/surviving-technical-due-diligence-ai-founder Published: 2026-06-30 Tags: AI startups, founders, engineering leadership > **TL;DR:** Every technical due diligence guide on the internet is written for the investor’s associate — a checklist of what *they* should poke at. You’re on the other side of that table, and nobody writes for you. So here’s the inverse. The reviewer opens your commit history before they open your README, and they’re reading it for *who actually built the core IP*, not how clever it is. In 2026 the AI-specific layer is where rounds get re-priced: model provenance, training-data rights, whether your eval is reproducible, and how badly you’re locked into one LLM vendor. And the part nobody will tell you because it doesn’t sell a checklist — there are gaps you should **not** scramble to fix, because a competent reviewer *expects* them at your stage. Panic-fixing them looks worse than the gap. The skill isn’t having no holes; it’s knowing which holes to name out loud with a plan. ## The checklists are all pointed the wrong way Search “technical due diligence” right now. Every result on the first page is written for the person doing the diligence — frameworks for VC associates, “30 questions to ask a startup’s CTO,” scorecards for evaluating an engineering team you’re about to wire money into. All of it is the *reviewer’s* manual. That’s useful if you’re the reviewer. It’s almost useless if you’re the founder about to be reviewed, because it tells you what gets checked but not what any of it *means*, what’s load-bearing versus theater, or what a “finding” actually does to your terms. I’ve sat on both sides of that table. I’ve been the technical reviewer an investor brings in to spend a week inside a startup’s repo and data room and write the memo that decides whether the round closes at the agreed price, closes lower, or quietly dies. And I’ve been the person sitting next to a founder for the two weeks *before* that, getting the house in order so the memo comes back clean. The view from those two chairs is very different, and the gap between them is exactly where founders lose leverage they didn’t have to lose. This is the founder’s-seat version. What the reviewer actually opens first, what they’re really reading it for, the AI-specific things that are new in 2026, the red flags that genuinely tank or re-price a round — and, honestly, the gaps you should leave alone. ## What the reviewer opens first (and it isn’t your pitch) Founders assume diligence starts with the architecture diagram. It doesn’t. A good reviewer goes to the evidence that’s hard to fake, and reads the polished stuff last. Here’s the actual opening sequence, roughly in order. **Commit history and the contribution graph.** This is the first thing I open, every time. Not the code — the *history*. `git log --author`, contribution-by-author over time, when the core files were written and by whom. It answers the one question diligence exists to answer: *does the team in the room actually own the IP they’re claiming?* A repo where 90% of the foundational commits came from one contractor who left, or from a co-founder who’s now “advising,” tells a story the pitch deck won’t. So does a history that starts three weeks ago with one giant “initial commit” of 40,000 lines — that’s either a migrated repo (fine, but I’ll want the old one) or a story I’m not being told. **Who wrote the core IP.** Related but distinct. I’m mapping the handful of files that *are* the company — the model-serving layer, the eval harness, the retrieval logic, whatever the moat actually lives in — to specific humans, and then checking those humans are full-time and on the cap table. The scary version: the genuinely novel part was written by someone who isn’t staying. **The README and onboarding path.** I will try to run your project. Not to judge your code style — to measure *how long until a new engineer is productive*. A README that gets me to a running local environment in under an hour signals a team that can hire and scale. A README that’s three stale commands and a Slack handle to ping signals key-person risk, because the system only runs in one person’s head. **Test coverage reality versus the claim.** Nobody expects 90% coverage at seed. But I’m checking whether tests exist *where the money is* — the billing path, the auth boundary, the core inference logic — versus 80% coverage concentrated on trivial utility functions to make a number look good. The gap between “we have good test coverage” in the data room and what’s actually covered is one of the most common credibility leaks I find. **Infra and secrets hygiene.** A thirty-second `git log -p` grep for high-entropy strings. Hardcoded API keys in the history, a `.env` committed in 2024, prod credentials in a Slack export — these aren’t just security findings, they’re *judgment* findings. They tell the reviewer how the team operates under deadline pressure, and the round is happening precisely because the team is under pressure. None of this is the impressive part of your company. That’s the point. Diligence is calibrated to find the gap between what you *say* and what’s *true*, and the boring artifacts are where that gap shows. ## The 2026 AI layer: where rounds actually get re-priced Everything above applies to any software company. What’s new — and what most founders are least prepared for — is the AI-specific diligence layer that’s become standard in 2026. This is where I’ve watched valuations move, because these are the questions a generalist reviewer didn’t know to ask three years ago and every competent one asks now. **Model and weights provenance.** If you fine-tuned or trained anything, where did the base model come from and what’s its license? An astonishing number of startups built a product on a base model whose license prohibits commercial use, or stacked a fine-tune on weights with a non-compete clause baked into the terms. I want a provenance chain: base model → license → your modifications → the license you’re shipping under. If that chain has a gap, your core asset has a legal cloud over it, and that’s a re-pricing event, not a footnote. **Training-data rights and the paper trail.** Where did your training and eval data come from, and can you prove you had the right to use it? Scraped data, data from a previous employer, customer data used for training without the contractual right to do so — this is the single fastest-growing category of finding I see, and it’s the one that scares acquirers most because it follows the company. “We scraped it and everyone does” is not a paper trail. You want documented sources, licenses or contracts, and a clean answer to “could a customer demand you delete their data from your model?” If the buyer is really after the team rather than the company, the exam is a different one entirely — [what reverse acqui-hires actually diligence](https://sublimecoding.com/blog/reverse-acqui-hire-technical-diligence). **Eval reproducibility.** Your deck says you beat the incumbent by 12 points on some benchmark. Can I *re-run* that eval? A reviewer who knows the space will ask for the eval harness and try to reproduce the number. If the benchmark only lives in a screenshot, or the harness has the test set leaking into context, or the number was cherry-picked from the best of twenty runs — that’s not just a credibility hit, it calls the entire technical claim into question. Reproducible evals are quietly becoming the strongest *positive* signal an AI startup can offer in diligence. Almost nobody has them tidy. Having them is a moat in the room. **Third-party LLM dependency and vendor lock-in.** How much of your product *is* a prompt to someone else’s model? That’s not automatically bad — most great AI products are wrappers around a foundation model plus real proprietary scaffolding. But the reviewer is sizing the risk: if your single provider 3x’s their price, deprecates the model you depend on, or ships your exact feature natively, what happens to your margins and your moat? The answer they want isn’t “we’d be fine” (nobody believes it). It’s evidence you’ve *thought about it* — an abstraction layer over the provider, a fallback model you’ve actually tested, an honest read on which parts of your value are yours versus rented. **Prompt-injection, output validation, and data isolation.** For anything agentic or anything touching customer data, the security posture is now a first-class diligence item, not an afterthought. Can a malicious input make your agent exfiltrate another tenant’s data or call a tool it shouldn’t? Is model output validated before it hits a database or an API, or do you trust the model’s text? Is tenant data isolated, or does everything share one context window and one vector store? You don’t need to be airtight at seed, but you need to demonstrate you understand the threat model. I’ve written more on what that posture looks like in practice in [running security at an AI-native company](https://sublimecoding.com/blog/running-security-at-an-ai-native-company-2026). **Inference cost and unit economics.** What does one unit of usage actually cost you in inference, and what’s the trend? A product that’s magical because it burns four dollars of tokens per user-session per day has a unit-economics problem dressed up as a product. The reviewer is checking whether your gross margin survives contact with scale, or whether growth makes the burn worse. Have the per-request cost, the trajectory as you’ve optimized, and an honest read on margin at 10x volume. ## The red flags that actually tank or re-price a round Not every finding is equal. Some get a note in the memo; some change the number. Here are the ones that genuinely move terms, why they scare a reviewer, and the cheap fix if you have time before the process starts. Red flag Why it scares the reviewer The cheap fix Core IP written by someone who’s gone Key-person risk on the actual moat; can you even maintain it? Document the system; get the departed contributor to sign a clean IP assignment if not already done Secrets in git history Judgment + security failure; what *else* is sloppy under pressure? Rotate every exposed credential now; don’t claim it was “never real” — own it and show the rotation Training data with no provenance Legal cloud over the core asset that follows the company Write the honest data-source memo *before* you’re asked; flag the gaps yourself Benchmark you can’t reproduce Calls every technical claim in the deck into question Build a clean, runnable eval harness; restate numbers conservatively “Test coverage is great” + tests only on trivia Credibility leak — what else is overstated? Just be precise about what’s covered; don’t inflate the claim 100% dependency on one LLM with no abstraction Margin and moat both controlled by a third party A thin provider abstraction + one tested fallback is enough to change the story No environment/tenant isolation in a multi-tenant AI product One injection away from a breach that ends the company Isolate tenant context and data stores; document the boundary The pattern across that whole table: the *technical* severity matters less than what the finding implies about the team. A reviewer extrapolates. One overstated claim makes them re-read every other claim with suspicion, and that suspicion is what re-prices a round — not any single bug. ## The contrarian part: gaps you should NOT panic-fix This is the section no investor-side checklist will ever write, because the entire genre exists to find problems. But it’s the most valuable thing I tell a founder before diligence, and it’s the part that builds trust precisely because it’s not trying to sell you a frantic remediation sprint. A competent reviewer is not looking for a startup with no gaps. A seed-stage startup with no technical debt, full test coverage, SOC 2, and a hardened internal admin tool is *suspicious* — either you’re lying, or you spent your runway on the wrong things instead of finding product-market fit. The reviewer is calibrating against your stage. So some gaps are not findings; they’re *expected*, and scrambling to paper over them right before the process makes you look like you don’t understand your own stage. Here’s the calibration I use: The “gap” What a reviewer actually expects at your stage What to do Some tech debt, a few TODOs, a known-ugly module Expected at seed and Series A. Its absence is the red flag. Leave it. Have a one-line answer for *why* it’s fine for now. No SOC 2 at seed Standard. SOC 2 is a revenue tool, not a security one, and pre-revenue it’s premature. Don’t start an audit to impress a reviewer. Know *when* you’ll need it. An internal tool that isn’t hardened Internal tooling is supposed to be scrappy. Note it’s internal-only and access-controlled; move on. Monolith instead of microservices A monolith at your stage is *correct*, not naive. Defend it as a deliberate choice, because it is one. Manual steps in deploy Acceptable early; reviewers care that you *know* which are manual. List them honestly. The list is the maturity signal. I’ve written separately about why [SOC 2 is a revenue tool, not a security tool](https://sublimecoding.com/blog/soc-2-is-a-revenue-tool-not-a-security-tool) — chasing it pre-revenue to look good in diligence is one of the most common ways founders burn runway on the wrong thing. The real skill isn’t having zero gaps. It’s **framing a known gap with a credible plan instead of hiding it.** Compare two answers to “your test coverage is thin on the inference path”: - *Hiding it:* “Oh, coverage is solid, we’re in good shape there.” — Now when the reviewer finds the gap (they will), every other claim you made is suspect. - *Framing it:* “Coverage on the inference path is intentionally light right now — we’ve been moving the model interface weekly and locking it down with tests would’ve slowed iteration. Now that the interface is stabilizing, here’s the two-week plan to get it covered, and here’s the one critical path that *is* tested today because a regression there bills customers wrong.” — Now the gap is *evidence of judgment.* Same gap. Opposite outcomes. The reviewer isn’t grading the codebase; they’re grading whether they can trust the founder’s self-assessment for the next several years. A founder who names their own weaknesses accurately is worth more than one with a slightly cleaner repo and a tendency to round up. ## A short pre-diligence prep checklist If you’ve got a process starting in the next month, this is what actually moves the needle — in priority order, not “best practices” order: - **Read your own commit history as a stranger.** Who wrote the core files? Is everyone who matters full-time and on the cap table? Any “initial commit” mysteries? Fix the *story*, not the code. - **Grep your git history for secrets.** `git log -p | grep`-style sweep for keys and `.env` files. Rotate anything you find. This is an hour and it’s pure downside protection. - **Write the data-provenance memo before you’re asked.** Every training/eval data source, the right you have to use it, and the honest gaps. Bringing this unprompted is a massive trust signal. - **Make your headline benchmark reproducible.** A clean eval harness a reviewer can run, with no test-set leakage. Restate any number you can’t reproduce. - **Write a one-page LLM-dependency honest take.** Which provider(s), what’s abstracted, what your fallback is, your per-request cost and margin trajectory. - **List your known gaps with plans.** The tech debt, the manual deploy steps, the un-hardened internal tool — written down, with a one-line “why it’s fine now / when we fix it” for each. This *is* the framing move, done in advance. - **Confirm the human story.** IP assignments signed by everyone who touched the core, including contractors and departed founders. This is the gap most likely to actually kill a deal. Notice what’s not on that list: a six-week refactor, a rushed SOC 2, a microservices migration. None of that survives the cost-benefit math right before a raise, and most of it makes you look like you’re optimizing for the wrong audience. If you’re staffing for the months *after* the round instead, that’s a different exercise — I’ve laid out how I think about it in the [pre-Series-A AI startup hiring plan](https://sublimecoding.com/blog/pre-series-a-ai-startup-hiring-plan). ## You shouldn’t do this alone Diligence is one of the few moments where a founder is being judged on a dimension — engineering depth, security posture, technical honesty — that most founders, even technical ones, haven’t had to perform under an adversarial expert before. The cost of getting it wrong is measured in valuation, and it’s concentrated into a single short window. This is exactly the kind of thing a fractional CTO or technical advisor does *with* you: reading your data room the way the reviewer will, finding the findings before they do, and helping you frame the real gaps with credible plans instead of hiding them. If you’ve got a process coming up, [that’s a conversation worth having early](https://sublimecoding.com/consulting) — before the data room opens, not after the memo comes back. --- ## Elixir Is the Language AI Codes Best URL: https://sublimecoding.com/blog/elixir-ai-codes-best Published: 2026-05-27 Tags: Elixir, AI tools, engineering > **TL;DR:** A Tencent benchmark across 20 languages found Elixir had the **highest LLM code-completion rate of any mainstream language** — 97.5% of problems solved by at least one model, with Claude Opus 4 hitting **80.3% on Elixir** vs **74.9% on C#** and **72.5% on Kotlin**. [Dashbit broke down why](https://dashbit.co/blog/why-elixir-best-language-for-ai). The reasons aren't a coincidence — they're the same boring properties that have always made Elixir pleasant to work in, now compounded by the fact that AI agents are writing more of your code every quarter. The strategic takeaway isn't "rewrite everything in Elixir." It's that the cost of choosing the trendy stack now includes "your AI tools will be measurably worse at it." ## The benchmark nobody saw coming I have a small reflex when a benchmark says my preferred stack wins: assume someone cooked the books. So when [Dashbit pointed at a Tencent study](https://dashbit.co/blog/why-elixir-best-language-for-ai) showing Elixir at the top of an LLM coding benchmark, my first move was to look for the catch. I didn't find one. The result is real. Across 20 languages, **97.5% of Elixir problems were solved by at least one model — the highest of any language tested.** Claude Opus 4 scored **80.3%** on Elixir, against **74.9%** on C# and **72.5%** on Kotlin. Those are not borderline gaps; in a benchmark where a few points decides the order, eight points is a moat. The puzzle is *why*. Elixir is not the most popular language. It is not in the top ten for Stack Overflow answers or GitHub commits. Models should have a lot less Elixir to learn from than, say, Python or JavaScript. And yet here we are. Dashbit's answer is the one I find convincing: the same language design choices that make Elixir nice to work in for humans turn out to be a *force multiplier* for the next decoder predicting the next token. None of it is about how much training data exists. It's about what the data looks like when there is some. ## What Dashbit's argument actually says The full piece is worth reading at the source, but the load-bearing claims are four. **Immutability gives models local reasoning.** In mutable languages, a function can quietly mutate an object passed in from a caller, and now reasoning about what the function does requires understanding everyone who might have touched the object first. Dashbit calls this "spooky action at a distance." Elixir doesn't allow it: anything a function needs is given as input, anything a function changes is given as output. A model — or a human — can predict the next few lines from the function signature alone. The pipe operator (`|>`) then makes the *flow* of transformations literal in the source. Local reasoning is cheap, and cheap is what models are good at. **Documentation is a first-class language feature.** Elixir distinguishes `@moduledoc` and `@doc` from inline comments. The doc strings are part of the language; they ship to HexDocs; they support `iex>` examples that are *also* executed as tests via doctests. The training corpus for Elixir is therefore unusually clean: a function's docs include a literal demonstration of its inputs and outputs, verified by CI to still be true. That is high-signal data per token. The recent addition of TypeSense-backed `mix hex.search` makes those docs version-aware, which matters more than it sounds — models trained on stale docs are wrong in confident, hard-to-detect ways. **Stability means training data ages well.** Elixir 1.0 shipped in 2014. It is still on 1.x. Phoenix is on 1.8; Ecto is on 3. The community treats deprecation warnings as the upgrade path, not breaking changes. Compare against any JavaScript framework you can name. Every blog post and tutorial about Elixir from the last decade is still mostly correct, which means the training data isn't polluted with contradictions between v2 and v15 advice for the same library. A model trained on "how to do X in Elixir" has one answer to learn, not five. **Tooling closes the agent feedback loop.** Compiled language, parallel compilation and tests, type *inference* (not full annotation) that catches the usual class of bugs without forcing ceremony, warnings rather than errors so iteration isn't blocked, and — critically — runtime introspection. You can inspect a live BEAM process's state, mailbox, and ancestry from a shell or programmatically. Tidewave's MCP server exposes that introspection to coding agents directly. The runtime is *legible* to an AI in a way Python or Node aren't, because it was built legible for humans first. That last property is the one nobody else is replicating. ## Why this compounds Pretend for a second the benchmark is right and accept the explanation. What does it mean for a team picking a stack in 2026? It means a tax that didn't used to exist now does. Every language choice already had a hiring cost, a library cost, a hosting cost. Now it has an *agent-effectiveness* cost, and that cost is not flat across stacks. If a model is eight points better at completing Elixir tasks than the runner-up — and your engineers now spend a meaningful chunk of their day reviewing model output, asking for changes, and watching agents do work autonomously — that delta walks straight into your team's throughput. It compounds because every PR your humans don't have to babysit becomes time spent on the next one. It compounds *again* because the AI getting things right means less context-switching back into "I'd better write this from scratch, the agent's confused." Friction tax dropped twice. This is the angle that's mostly missing from "which language should we use" debates. People still talk about ergonomics and ecosystems and hiring. Those matter. They no longer fully describe the cost function. The boring, stable, well-documented language that *AI models reason about cleanly* is now strictly more valuable than it used to be, and the trendy framework with twelve breaking changes a year is strictly more expensive. The kind of code Elixir produces — small, isolated, well-typed by signature, documented in a format that's part of the language — is what AI is good at: `defmodule Inventory do @moduledoc "Stock-level operations for product SKUs." @doc """ Decrements stock for a SKU by `qty`. Returns `{:ok, new_stock}` or `{:error, :insufficient_stock}` when the request would go negative. ## Examples iex> Inventory.decrement(%{"WIDGET-1" => 5}, "WIDGET-1", 2) {:ok, 3} iex> Inventory.decrement(%{"WIDGET-1" => 1}, "WIDGET-1", 5) {:error, :insufficient_stock} """ def decrement(stock, sku, qty) when qty > 0 do case Map.get(stock, sku, 0) do current when current >= qty -> {:ok, current - qty} _ -> {:error, :insufficient_stock} end end end ``` Ten lines of executable spec — the docstring *is* the test, the signature is the contract, the function is pure, the failure case has a tagged tuple instead of an exception. A model reading this sees a complete, verifiable unit. There is nothing offstage. That is the shape of Elixir at rest, and the shape AI handles best. ## The honest costs If Elixir-for-AI were free, every team would already be on it. It isn't. We ship Elixir in production for clients, and the costs are real. **The hiring pool is still smaller.** This was the leading objection in 2018 and it's still the leading objection in 2026. You will interview fewer senior Elixir engineers than senior Go or Node engineers, full stop. The mitigation — "strong devs pick it up in two weeks" — is true and we've watched it work, but you cannot run a hiring strategy on it if you need three engineers next month. **Ecosystem corners still gap.** Phoenix, LiveView, and Ecto are excellent. Outside that core: certain cloud SDKs are thinner than the Python or Go equivalents, niche protocol clients sometimes don't exist, and you'll occasionally write a NIF or shell out to another runtime. None of that is fatal; all of it is work you wouldn't have on a mainstream stack. **The BEAM isn't a number-crunching runtime.** This is the one the AI conversation specifically muddles. Elixir is *excellent* for orchestrating AI workloads — calling models, streaming tokens to a LiveView UI, supervising long-running inference jobs, fanning out across providers. It is *not* where you run your matrix math. Nx` and `EXLA` give you a real answer for numerical work via XLA, and they're impressive, but they're an escape hatch into compiled native code — not a claim that pure Elixir is fast at tensor ops. Get the framing right or your benchmarks will lie to you. **Erlang-isms leak through.** Stack traces drop into Erlang term syntax. Docs split across Elixir *and* Erlang/OTP. Eventually you read Erlang source to understand a library. That tax is paid by every engineer, forever — not just at onboarding. ## Verdict The Tencent number is real, the Dashbit explanation is the right one, and the strategic implication is bigger than the benchmark headline suggests. You are not choosing a language for humans anymore. You are choosing a language for humans *and* the increasingly autonomous tools they work alongside. The properties that have always made Elixir pleasant — immutability, doc culture, version stability, runtime introspection — turn out to be the exact properties that make AI good at it, too. That is not a coincidence; it is a design philosophy paying off twice. Pick Elixir for the reasons you would have picked it anyway: a stateful backend where things must fail independently, real-time UI without three layers of glue, a team that wants to ship boring code that ages well. Then notice that your AI assistants are also measurably better at it. Then pay the hiring tax with your eyes open, keep it away from your matrix math, and let it compound. *Source: ["Why Elixir is the best language for AI" — Dashbit](https://dashbit.co/blog/why-elixir-best-language-for-ai).* --- ## Elixir's BEAM Is the Runtime AI Agents Want URL: https://sublimecoding.com/blog/elixir-beam-ai-agents Published: 2026-05-31 Tags: Elixir, agents, AI > **TL;DR:** The AI-agent language argument has two camps and both are right about the wrong layer. Go won the *transport* — the MCP server, the daemon, the thing that ships as one binary. Python owns the *reasoning* — the prompts, the evals, the model glue. But there’s a third layer nobody is fighting over, and it’s the one that actually hurts in production: keeping thousands of long-lived, stateful agents alive while they crash, retry, and hold conversation state for hours. That’s not a daemon problem and it’s not a prompt problem. It’s a *supervised-process* problem, and the BEAM — Erlang’s virtual machine, the one Elixir runs on — has been the best tool on earth for it since before the word “agent” meant this. Here’s the honest version, including where the BEAM is the wrong call. ## The layer nobody named A while back I [pulled apart why every AI agent framework is written in Go](https://sublimecoding.com/blog/go-ai-agent-frameworks). The argument held up: an agent *tool* — an MCP server, a CLI, an orchestrator — is mechanically a concurrent network daemon that has to ship as one file into someone else’s machine, and Go is unreasonably good at exactly that. The reasoning content stays in Python. The conclusion was a split: **Go owns the daemon, Python/TS owns the reasoning, talk over a wire between them.** That post named two layers and quietly walked past a third. I even flagged the crack at the time — that a panic in one goroutine “is not isolated the way people assume,” and pointed at the [failure mode I went deep on in Elixir’s concurrency model](https://sublimecoding.com/blog/elixir-concurrency-model). That crack is the whole subject of this post. Because here is the thing an MCP server is *not*: it is not the agent. The MCP server is plumbing — it answers `tools/call` and goes back to sleep. The agent is the thing on the *other* side of the model call: the long-lived entity that holds a conversation, remembers what it was doing, calls six flaky tools in sequence, gets a malformed response from one of them, and has to either recover or die without taking its eleven thousand siblings down with it. That entity isn’t a daemon and isn’t a prompt. It’s a *process with a lifecycle*, and which language’s runtime owns that layer is a question both camps skipped. ## What an agent actually is, mechanically Strip the word “AI” off an agent the same way we stripped it off the tool, and describe what’s left to a backend engineer. It’s an entity that starts up when a user begins a session. It holds state — the conversation, the scratchpad, the half-finished plan — for the *entire* duration of that session, which might be seconds or might be hours. While it’s alive it makes a sequence of unreliable calls: model APIs that rate-limit and time out, tools that throw, subprocesses that hang. Some fraction of those calls will fail in ways you did not anticipate, because the failure is coming from a stochastic model deciding to emit malformed JSON, or a third-party API having a bad afternoon. When one of those failures happens, the *correct* behavior is almost never “take down the server.” It’s “this one agent’s current step failed; retry it, reset it to its last good state, or let this one agent die — and leave the other ten thousand completely untouched.” Now describe *that* to someone who wrote telecom software in the nineties and they will name the primitive before you finish the sentence: it’s a supervised process. One process per agent, holding its own isolated state, linked to a supervisor that knows what to do when it dies. You did not invent a new architecture for AI. You rediscovered OTP. This is the tell. The shape of “many independent, stateful, long-lived, failure-prone things that must be isolated from each other” is not new and was never about AI. It’s the shape of phone calls, of chat sessions, of multiplayer game state — the workloads the BEAM was *purpose-built* for. Agents just happen to have that exact shape, and most teams are discovering it the hard way by reimplementing supervision badly in a runtime that doesn’t have it. ## Why the BEAM fits the shape Four properties, each of which is a direct answer to a problem the agent workload creates. None of them is an AI feature — that’s the point, the same way Go didn’t win the daemon by being good at AI. **Real process isolation, not the goroutine kind.** A BEAM process has its own heap, its own stack, and its own garbage collector. Nothing is shared by default; processes communicate only by copying messages. The practical consequence is the one that matters for agents: when a process crashes, the blast radius is *that process*. Its memory is reclaimed, its siblings never notice. Contrast the goroutine, which I was careful about in the [Go post](https://sublimecoding.com/blog/go-ai-agent-frameworks): goroutines share an address space and an unrecovered panic in one goroutine takes down the entire OS process — every other in-flight agent with it. You can paper over that with `recover()` at every boundary, but you are hand-rolling, imperfectly, the isolation the BEAM gives you for free. For a system whose defining characteristic is “individual units fail constantly and unpredictably,” shared-fate concurrency is the wrong default and isolated-fate concurrency is the right one. **Supervision trees turn “let it crash” into a retry strategy.** OTP’s supervision model — a tree of supervisor processes whose only job is to start, monitor, and restart their children according to a declared policy — came out of Ericsson’s work on systems that were not allowed to go down. Joe Armstrong’s 2003 thesis was literally titled *Making reliable distributed systems in the presence of software errors*, and the AXD301 switch built on these ideas famously reached availability figures quoted in the nine-nines range. The philosophy is “let it crash”: don’t litter defensive `try/catch` through your business logic trying to anticipate every failure; let the process die cleanly and let a supervisor restart it from a known-good state. Read that sentence again with an agent in mind. An agent step that fails on a bad model response *should* crash and restart from its last checkpoint — that’s not a workaround, it’s the designed-for case. The retry-with-backoff, reset-to-checkpoint, escalate-after-N-failures logic that agent frameworks in other languages write by hand is a `Supervisor` strategy you *declare* on the BEAM. **One process per agent is genuinely free.** The objection to “a process per agent” in most runtimes is cost — OS threads are expensive, so you pool and multiplex and lose the isolation. On the BEAM the objection evaporates. A freshly spawned process starts at roughly 2–3 KB of memory and is created in microseconds; a single node sustains hundreds of thousands to millions of concurrent processes without breaking a sweat. So you don’t pool agents onto shared workers and reintroduce shared fate. You give every agent — every conversation, every sub-task, every tool invocation if you want — its own process, with its own state and its own crash domain, and you do it by the hundred thousand. The architecture you’d *want* on paper (total isolation) is also the cheap one, which is not a tradeoff you usually get to make. **Preemptive scheduling means one wedged agent can’t starve the rest.** The BEAM scheduler is preemptive: it counts reductions (roughly, work units) and forcibly yields a process after a small budget, so no single process can monopolize a scheduler thread. For agents this matters more than it first looks. A long-running agent that does something CPU-heavy — parsing a huge document, a tight retry loop, a runaway tool — cannot wedge the runtime and freeze every other agent’s progress. Go’s scheduler is cooperative at the edges and a tight CPU loop can misbehave; Python’s GIL serializes CPU-bound work outright (free-threading is coming, but it isn’t the world most agent code runs in today). The BEAM’s “everyone gets a fair slice, always” is the property you want when you’re running a noisy crowd of independent agents of wildly varying behavior on one box. Put those four together and you have a runtime whose native unit *is* the long-lived, isolated, supervised, fairly-scheduled stateful process. That is the agent, described exactly. ## It’s not vaporware The fair pushback is “great theory, but is anyone actually building agents on this, or is it a forum argument?” The ecosystem is real and getting realer fast — I watched the [SERPs fill with it](https://sublimecoding.com/blog/elixir-ai-codes-best) while researching this. [**Jido**](https://github.com/agentjido/jido) is an OTP-native autonomous-agent framework: agents are supervised processes with an immutable functional state model, and the AI layer is optional — the core gives you the agent *architecture* (planning, actions, lifecycle) and you bolt the LLM on. That factoring is the whole thesis of this post shipped as a library. **LangChain for Elixir** — the `langchain` Hex package maintained by Mark Ericksen at Fly.io — is the pragmatic model-integration layer: a clean client for OpenAI, Anthropic, and the rest, with tool-calling and structured output, so the “reasoning” wire from the Go post terminates somewhere sane in Elixir. And because Phoenix is right there, streaming a model’s tokens to a live UI is not a separate websocket stack you stand up — [LiveView already holds the connection](https://sublimecoding.com/blog/elixir-concurrency-model), and async assigns push tokens as they arrive. The “simplest real-time AI UI” is close to free when the agent and the UI live in the same supervised tree. Which structured-output library to reach for, and why the obvious one is not the one shipping, is [its own comparison](https://sublimecoding.com/blog/structured-llm-output-elixir). I’ll keep the scope honest: this ecosystem is younger and smaller than Python’s, and you will occasionally be the first person to hit a rough edge. But the *primitives* — processes, supervisors, message passing — are thirty years mature. The agent libraries are thin, sane layers over a deep foundation, which is the opposite of the usual situation where a slick library hides a shaky core. ## What it costs you Here’s the part the title doesn’t say, the same way the Go post owed you the costs of choosing Go. The BEAM is weak in exactly the place Python is strong, and pretending otherwise is how you talk yourself into a bad architecture. **The model and ML layer is not the BEAM’s.** If your agent needs to run inference *locally* — embeddings, a local model, real tensor math — you are swimming against the current. [Nx](https://github.com/elixir-nx/nx) and [Bumblebee](https://github.com/elixir-nx/bumblebee) exist and are genuinely impressive work, and you can run Whisper or a Llama-class model from Elixir today. But the frontier of models, the day-one SDKs, the research code, the eval tooling, the sheer gravity of the ecosystem — that’s all Python, and it will be for years. If the *center of mass* of your system is the model itself rather than the orchestration of agents around it, you are buying the wrong runtime to save the wrong cost. **The talent pool is smaller.** You will hire Elixir engineers more slowly than Python or Go engineers, full stop. For a lot of teams that single fact outweighs every architectural elegance in this post, and it should — the [most pragmatic stack is frequently the boring one you can staff](https://sublimecoding.com/blog/ruby-boring-ships). Be honest with yourself about whether you’re optimizing for the system’s properties or for your own enjoyment of them. **You’re calling out for the model anyway.** In the overwhelmingly common case, your “model call” is an HTTPS request to a hosted API. That’s true in every language, which means the BEAM’s weakness at *local* inference is irrelevant to most production agents — but it also means the model layer isn’t where your language choice pays off, so don’t let “but Python has the SDKs” decide a system whose hard problem is supervising ten thousand stateful sessions, not calling an API. Notice the costs all cluster in the same place — the model/reasoning layer — exactly as Go’s costs all clustered in *its* reasoning layer. The BEAM’s weaknesses are Python’s strengths. Which is the entire point of the next section. ## The split that completes the trilogy The Go post ended with a two-way split. With the third layer named, it’s a clean three-way one, and the seams fall in obvious places: Layer What it is Right tool Why **Transport / daemon** MCP server, CLI, the thing users install **Go** One binary, near-zero deps, cheap concurrency — its home turf **Reasoning / model** Prompts, evals, inference, ML glue **Python / hosted API** REPL loop, the SDKs, the entire ML ecosystem **Stateful supervision** Long-lived agents, session state, crash recovery, fan-out **BEAM (Elixir)** Isolated supervised processes are the *native unit* The decision rule, not the language-war version: pick your runtime by **where your system’s hard problem actually lives.** If the hard problem is “ship a tool into a thousand machines,” that’s Go. If it’s “iterate on prompts and run a model,” that’s Python. If it’s “keep an enormous number of independent, stateful, failure-prone agents alive and isolated for hours,” that’s the BEAM, and it’s not close. Most real systems are more than one of these, and the mature answer is the same as it was for daemon-versus-reasoning: don’t force one language across a seam it doesn’t belong on. Let the BEAM supervise, let Python think, let Go ship the binaries, and put wires between them. ## The part everyone’s been arguing past The whole “what language for AI agents” debate has been a fight about the two visible layers — the binary you install and the prompt you tune. Both matter and both have clear winners. But the layer that actually decides whether your agent platform survives contact with production isn’t either of those. It’s what happens at 3 a.m. when four hundred agents are mid-task and the model API starts returning garbage: do four hundred sessions die, or does one runtime quietly crash-and-restart each failed step from its last checkpoint while everything else keeps running? That problem — many isolated stateful things failing independently and recovering without a global blast radius — was solved, productized, and battle-hardened for telephone switches before most of us were writing code. The agent era didn’t create a new hard problem at the orchestration layer. It walked straight into an old one that already has a famously good answer. The only surprising thing is how few people building agents have noticed that the runtime they want already exists, has for decades, and is sitting one `mix new` away. Use the BEAM for the thing the BEAM is for. Let it hold the agents. Just don’t ask it to run your model — keep that where it belongs, on the other side of a wire, in the language built for it. --- ## Elixir's Concurrency Model Is the One You Actually Want URL: https://sublimecoding.com/blog/elixir-concurrency-model Published: 2026-05-16 Tags: elixir, otp, engineering > **TL;DR:** `async/await` and goroutines solve *scheduling* — how to interleave a lot of work on a few OS threads without blocking. The BEAM solves *failure* — what happens when one of those units of work blows up at 3am. Most of the "concurrency" pain backend devs feel is actually failure-isolation pain wearing a concurrency costume, and only the actor model plus supervision trees address it head-on. Elixir's model is the one you actually want; it's also the one with the smaller hiring pool, the Erlang-isms, and no business doing your matrix multiplication. Here's the honest version, with a working GenServer and Supervisor you can paste into a fresh `mix` project. ## The 3am page async/await can't prevent Here's a bug I've shipped, in some form, in three different languages. A request handler does five things. It validates input, hits the database, calls a third-party API, transforms the result, and writes to a cache. The third-party API starts returning a malformed payload — not an HTTP error, just JSON with a field that's now `null` where it used to be a string. Your transform step does `payload.token.toUpperCase()`. It throws. The throw is unhandled in that code path because you wrote the happy path first and the deadline was Friday. In Node, depending on where that ran, you either crash the process or — worse — you reject a promise nobody's awaiting and the runtime prints `UnhandledPromiseRejection` and, in modern Node, exits anyway. In Python with `asyncio`, an exception in a task that nobody `await`s gets logged when the task is garbage-collected and silently swallowed until then. In Go, if that transform ran in a bare `go func()` with no `recover()`, the panic walks up that goroutine's stack and takes the entire process with it. One bad upstream payload, one unguarded line, whole service down. The pager goes off at 3am. Notice what the bug *isn't*. It isn't a scheduling problem. `async/await` scheduled that work perfectly. Goroutines would have scheduled it perfectly. The event loop did its job. The bug is a *failure-isolation* problem: there was no boundary between "this one request's transform step exploded" and "the process serving every other request is now dead." This is the thing I want to convince you of: the concurrency model you reach for should be judged less on how elegantly it schedules work and more on what it does when one unit of that work fails. By that measure, the models most backend devs use every day are weak, and the one they keep getting told to try — "just use Elixir" — is strong for reasons nobody bothers to explain past the slogan. Let me explain past the slogan. ## The three models, honestly Strip the marketing off and there are three concurrency models a backend dev is likely to touch. They are not competing implementations of the same idea. They guarantee different things. **Threads and async (Node, Python, the JVM's default style).** You have one or a few OS threads. You multiplex many logical tasks onto them using an event loop (`libuv`, `asyncio`) or a thread pool. Tasks share the same heap. The model's core guarantee is *throughput*: you can have ten thousand in-flight requests without ten thousand OS threads. What it does *not* give you is isolation. Every task lives in the same memory space and, in single-threaded runtimes, the same failure domain. An unhandled exception's blast radius is "whatever shares this thread/process," which in practice is everything. You bolt safety on afterward with `try/catch` discipline, framework-level error middleware, and a process supervisor like `pm2` or systemd restarting the whole thing. **Goroutines and channels (Go).** Go gives you cheap green-threaded units (goroutines) multiplexed by the runtime onto OS threads, plus channels for communicating between them. This is genuinely better ergonomics than callback or async-coloring soup — goroutines don't have a "color," and `go foo()` is about as low-friction as concurrency syntax gets. The guarantee is *cheap concurrency with first-class communication*. What Go does not give you is memory isolation or failure isolation. Goroutines share the same address space by design; the language even documents the data-race rules you must follow because shared memory is the default substrate. And an unrecovered panic is not goroutine-local — it terminates the program. **Processes and supervision (the BEAM: Erlang, Elixir).** The BEAM gives you *processes* — not OS processes, not threads, but VM-level units that are extraordinarily cheap and, critically, **share no memory**. The Erlang docs are blunt about it: "Threads of execution in Erlang share no data, that is why they are called processes." ([erlang.org, Concurrent Programming](https://www.erlang.org/doc/system/conc_prog.html)) They communicate only by copying messages between isolated mailboxes. The guarantee here is different in kind: *isolation plus a structured story for failure*. A process can crash without touching any other process's memory, because there is no shared memory to touch. And the platform ships a first-class abstraction — supervision trees — whose entire job is deciding what to do when one does crash. The first two models optimize "run lots of things without blocking." The third optimizes "contain the damage when one of those things dies." Those are different problems, and most production incidents are the second one. ## Where Go's model leaks I'm picking on Go specifically because Go is the language people most often reach for when they've outgrown Node/Python concurrency and *think* they've solved the problem. Go's model is good. It is not the same kind of good. **Shared memory is the default, not the exception.** The Go proverb is "Do not communicate by sharing memory; instead, share memory by communicating" ([go.dev, Share Memory By Communicating](https://go.dev/blog/codelab-share)). It's good advice precisely because the default is the opposite. Channels are opt-in; the shared heap is opt-out. The Go memory model exists to tell you the rules for the unsafe thing you can do by accident: it defines a data race as "a write to a memory location happening concurrently with another read or write to that same location" and is explicit that races are errors — its own summary of the philosophy is "Don't be clever." ([go.dev, The Go Memory Model](https://go.dev/ref/mem)) A model whose spec needs a "don't be clever" section is a model where the foot-gun is loaded by default. **A panic in one goroutine takes down all of them.** This is the big one and it surprises people coming from "concurrency means isolation." It does not, in Go. From the official Go blog: when a panic isn't recovered, "the process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes." ([go.dev, Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover)) And `recover` only works from a deferred function *on the same goroutine that panicked* — a sibling goroutine cannot catch it for you. So the moment you write `go handleRequest(conn)` and `handleRequest` panics on a nil dereference from that malformed upstream payload, every other in-flight request in that process dies with it. The mitigation is real but it's manual: you wrap every goroutine entry point in `defer func(){ recover() }()`. Forget one — in a library, in a callback, in code a teammate wrote on a Friday — and you're back to whole-process death. Isolation that depends on every author remembering a boilerplate incantation is not isolation; it's a convention. **Channel deadlocks are a class of bug, not an edge case.** Unbuffered channel sends block until there's a receiver. A goroutine waiting to send on a channel nobody will ever receive from is stuck forever, holding whatever it holds. If *every* goroutine ends up blocked this way the Go runtime can detect total deadlock and crash with `fatal error: all goroutines are asleep - deadlock!` — but the far more common production case is a *partial* deadlock: a few goroutines wedged on channel operations while the rest of the program runs fine, leaking a little memory and one request's worth of progress every time it happens, invisible until you're staring at a slowly climbing goroutine count in production. None of this makes Go bad. Go's model is a massive upgrade over callback-era Node for the *scheduling* problem. It just doesn't solve the *failure-isolation* problem, and it's frequently sold as if it does. ## The BEAM bet The BEAM makes a specific, opinionated bet: optimize the runtime for *isolated failure and structured recovery*, and accept the costs that come with it. **Processes are cheap and isolated.** A freshly spawned BEAM process is small. The Erlang efficiency guide states the default initial heap is 233 words and notes this is "quite conservative to support Erlang systems with hundreds of thousands or even millions of processes"; the same guide's worked example shows a newly spawned process at 327 words total ([erlang.org, Processes](https://www.erlang.org/doc/system/eff_guide_processes.html)). A word is 8 bytes on a 64-bit VM, so we're talking single-digit kilobytes per process, growing on demand. The point isn't a brag number — it's that "spawn a dedicated process per request, per connection, per job" is a normal, expected thing to do, not a resource gamble. Each one has its own heap. A crash in one cannot corrupt another's state because there is no shared state to corrupt. **Preemptive scheduling, so one process can't starve the rest.** Go's scheduler is good but its preemption story has historically had rough edges. The BEAM is preemptive by reduction counting: a process is given a fixed budget of "reductions" (roughly, function calls) and yielded when it's spent them. The budget is the `CONTEXT_REDS` constant — 4000 reductions — defined in the VM's `erl_vm.h` and documented in *The BEAM Book* ([happi/theBeamBook, scheduling](https://github.com/happi/theBeamBook/blob/master/chapters/scheduling.asciidoc)). Practically: one process running a tight loop cannot freeze the others. The scheduler will pull the rug at 4000 reductions whether the code cooperates or not. That property is *why* one slow request doesn't degrade the latency of the other ten thousand. **"Let it crash" and supervision trees.** This is the philosophical core, and it's the opposite of defensive programming. Instead of wrapping every operation in error handling to keep a process limping along in a corrupted state, you let the process *die* cleanly at the first sign that its assumptions are violated — and you put a supervisor above it whose job is to restart it from a known-good initial state. The OTP design principles describe this directly: "The supervision tree is a hierarchical arrangement of code into supervisors and workers, which makes it possible to design and program fault-tolerant software." ([erlang.org, OTP Design Principles](https://www.erlang.org/doc/system/design_principles.html)) A supervisor "is responsible for starting, stopping, and monitoring its child processes," with restart strategies (`:one_for_one`, `:one_for_all`, `:rest_for_one`) that declare exactly how a sibling's death affects the others ([erlang.org, Supervisor Principles](https://www.erlang.org/doc/system/sup_princ.html)). Here's the thing that's hard to convey until you've run it: in this model, the 3am bug from the opening *is not a page*. The request process handling the malformed payload crashes. Its supervisor restarts a fresh worker. Every other request is untouched because it was a different process with a different heap. You get a log entry and a metric, not an outage. The failure didn't have to be anticipated, caught, and handled at the call site. It had to be *contained*, and containment is the runtime's job, not yours. Here is a real, working example — a rate-limiter `GenServer` supervised by a `Supervisor`. Paste it into `lib/` of a fresh `mix new demo` project and it runs. `defmodule Demo.RateLimiter do @moduledoc """ A token-bucket rate limiter as an isolated process. If its state ever becomes inconsistent it is allowed to crash; the supervisor restarts it from a clean bucket. """ use GenServer # --- Client API --- def start_link(opts) do name = Keyword.get(opts, :name, __MODULE__) GenServer.start_link(__MODULE__, opts, name: name) end @doc "Returns :ok if a token was available, :rate_limited otherwise." def request(server \\ __MODULE__) do GenServer.call(server, :request) end # --- Server callbacks --- @impl true def init(opts) do max = Keyword.get(opts, :max_tokens, 5) refill_ms = Keyword.get(opts, :refill_ms, 1_000) :timer.send_interval(refill_ms, :refill) {:ok, %{tokens: max, max: max}} end @impl true def handle_call(:request, _from, %{tokens: tokens} = state) when tokens > 0 do {:reply, :ok, %{state | tokens: tokens - 1}} end @impl true def handle_call(:request, _from, state) do {:reply, :rate_limited, state} end @impl true def handle_info(:refill, %{max: max} = state) do {:noreply, %{state | tokens: max}} end end defmodule Demo.Application do @moduledoc false use Application @impl true def start(_type, _args) do children = [ # If RateLimiter crashes, ONLY RateLimiter is restarted. {Demo.RateLimiter, name: Demo.RateLimiter, max_tokens: 5, refill_ms: 1_000} ] opts = [strategy: :one_for_one, name: Demo.Supervisor] Supervisor.start_link(children, opts) end end ``` Wire Demo.Application` into `mix.exs` with `mod: {Demo.Application, []}`, run `iex -S mix`, and try it: `iex> Demo.RateLimiter.request() :ok # ...call it past the bucket size... iex> Demo.RateLimiter.request() :rate_limited # Now kill it on purpose and watch the supervisor heal it: iex> Process.exit(Process.whereis(Demo.RateLimiter), :kill) true iex> Demo.RateLimiter.request() :ok # a brand-new process, fresh full bucket, no manual restart ``` That Process.exit(..., :kill)` is the whole argument in three lines. You deliberately destroyed the process. You did not write a single line of recovery code. The `:one_for_one` supervisor noticed the child died and started a clean replacement, and the very next call succeeds against fresh state. `GenServer` is, in the official Elixir docs' words, "a behaviour module for implementing the server of a client-server relation" that plugs directly into supervision and standard error reporting ([hexdocs.pm, GenServer](https://hexdocs.pm/elixir/GenServer.html)). You write the state transitions; OTP writes the resilience. That is the bet: a little ceremony (`GenServer` callbacks, child specs, supervision strategy) bought up front, in exchange for failure isolation being a *structural property of the system* rather than a discipline every author must remember. ## The honest costs If the BEAM model were free, everyone would use it. It isn't. A flagship post that doesn't say this is a brochure. **The hiring pool is genuinely smaller.** This is the real one, and no amount of "but it's easy to learn" hand-waving makes it go away. You will interview fewer Elixir engineers than Go or Node engineers, full stop. You can mitigate it — the language is approachable and strong devs pick it up fast — but if your hiring strategy depends on a deep local market of people who already know the stack, that's a strike against, and pretending otherwise is dishonest. **Ecosystem gaps in specific corners.** The web story (Phoenix, LiveView, Ecto) is excellent and competitive with anything. Outside that, you will hit libraries that are thinner than the Go or Python equivalent: certain cloud-vendor SDKs, some ML/data tooling, niche protocol clients. The usual escape hatch is a port/NIF or shelling out to another runtime, which is fine but it's work you wouldn't have on a more mainstream stack. **Erlang-isms leak through.** Elixir is a lovely language, but it sits on a 1980s telecom VM, and the substrate shows. Stack traces drop into Erlang term syntax. Tooling and observability docs are split across Elixir *and* Erlang/OTP. You will, eventually, read Erlang source to understand a library. That's a tax on every engineer, paid forever, not just at onboarding. **It is not a number-crunching runtime.** The BEAM is optimized for massive concurrency and message passing, not raw CPU throughput on tight numeric loops. Heavy computation — image processing, large-matrix math, cryptographic grinding — is not what it's for, and naively doing it in pure Elixir is slow. The community answer is real (NIFs, `Nx`/`EXLA` for numerical work, offloading to native code), but the honest framing is: the runtime's strength is concurrency-and-failure, and you pay for that focus in compute-bound work. ## When NOT to reach for Elixir Decisions are made by knowing when *not* to use the thing. - **CPU-bound batch work with little concurrency.** A nightly job that does heavy math on one big dataset has no failure-isolation problem worth solving and will run faster in a runtime built for throughput. Wrong tool. - **Small teams on a tight deadline who already know Go/Node/Python well.** The right concurrency model in your hands beats the better one you're learning under deadline pressure. Familiarity is a real engineering input. Ship the thing. - **You genuinely don't have a failure-isolation problem.** A mostly-stateless service that fans out a few HTTP calls behind a load balancer that already restarts unhealthy instances has externalized the supervision problem to your orchestrator. The BEAM's biggest advantage is partly redundant there. It's still nice; it's not decisive. - **Hard real-time or microsecond-latency systems.** The BEAM's preemptive, garbage-collected scheduling is built for soft real-time (consistent low-ish latency under massive concurrency), not hard guarantees. If you need bounded microsecond worst-case, this is not your platform. The pattern: reach for the BEAM when your dominant pain is *many independent things that can each fail independently and must not take each other down*. That's chat backends, telephony, IoT fleets, payment orchestration, real-time multiplayer, anything stateful-per-connection. When that's not your shape, the model's signature advantage is muted, and its costs are still fully priced in. (Network config automation is that shape too — [Cisco's NSO quietly runs its core on an Erlang VM](https://sublimecoding.com/blog/cisco-nso-secretly-erlang).) ## Verdict The slogan "just use Elixir" is right for the wrong stated reason. People say it like it's about concurrency throughput. It isn't — Go and well-written async Node handle enormous concurrency. It's about what the runtime does at 3am when one unit of that concurrency hits a bug nobody anticipated. `async/await` and goroutines schedule beautifully and then make that failure your problem, manually, forever. The BEAM makes it the runtime's problem, structurally, by default. Here's the decision table I actually use: Your dominant pain Best-fit model Why "I have lots of I/O-bound requests and don't want a thread per request" Threads/async (Node, Python, JVM) Scheduling is the whole problem; isolation isn't your bottleneck "I want cheap concurrency with clean communication and a big hiring pool" Goroutines + channels (Go) Excellent scheduling + ergonomics; accept manual failure isolation "Independent stateful things that must fail without taking each other down" Processes + supervision (Elixir/BEAM) Failure isolation is structural, not a convention you must remember "Heavy CPU-bound numeric work, low concurrency" A throughput runtime (Go, Rust, native) None of the above models' isolation buys you anything here "Hard real-time, bounded worst-case latency" A real-time runtime, not the BEAM GC + preemptive scheduling is soft real-time only If your honest answer to "what's my dominant pain" is the third row — and for a surprising number of stateful backends it is, people just file it under "concurrency" because that's the word they have — then yes. It's the model you actually want. Pay the hiring and Erlang-ism costs with your eyes open, keep it away from your matrix math, and let it crash. --- ## Why Every AI Agent Framework Is Written in Go (And What That Costs You) URL: https://sublimecoding.com/blog/go-ai-agent-frameworks Published: 2026-05-16 Tags: go, agents, engineering > **TL;DR:** Open the repos behind the agent tooling you actually run — Ollama, the MCP SDKs, the orchestration engines — and you keep landing on the same language: Go. Not because Go is good at AI; it isn't, particularly. Because the *thing* an agent tool actually is — a long-lived concurrent network daemon that has to ship as one binary your users can run without a runtime — is exactly what Go was built for. That's a great trade for the daemon and a bad one for the reasoning layer, which is why the ecosystem is quietly splitting in two: Go for the process, Python/TS for the prompt logic. Here's the honest version, with a stdlib-only MCP server you can compile to a single file. ## The roll-call Go look at what you're running. If you've followed along with the [Claude Code Resource Bible](https://sublimecoding.com/blog/claude-code-resource-bible), you've already installed half of this list without noticing the pattern. Ollama, the thing you `brew install` to run a local model, is Go — a single binary, no Python environment to break ([github.com/ollama/ollama](https://github.com/ollama/ollama)). The Model Context Protocol's official Go SDK is Go, maintained in collaboration with Google ([github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk)). The most-used community MCP library, `mark3labs/mcp-go`, is Go ([github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go)). CloudWeGo's Eino — billed flatly as "the ultimate LLM/AI application development framework in Go" — is Go ([github.com/cloudwego/eino](https://github.com/cloudwego/eino)). LangChainGo is Go ([github.com/tmc/langchaingo](https://github.com/tmc/langchaingo)). Drop down a layer to the orchestration substrate agents run *on* and it's the same story: Temporal, the durable-execution engine a lot of agent workflows are built on, is Go ([github.com/temporalio/temporal](https://github.com/temporalio/temporal)); Dagger, the automation engine that grew agent modules, is Go ([github.com/dagger/dagger](https://github.com/dagger/dagger)); Docker and Kubernetes — the things the whole circus is shipped and scheduled in — are Go. The reasoning *content* of all of this — the models, the research, the prompt engineering, the eval harnesses — is overwhelmingly Python. But the *machinery* that delivers it to you is overwhelmingly Go. That split is not an accident and it is not taste. It's an architectural tell, and once you see it you can't unsee it. The title of this post is the argument. The rest of it earns the claim, and then tells you what believing it costs you — because the same properties that make Go the right call for the daemon make it an actively annoying place to write the part of an agent that thinks. ## Why Go fits the shape of the problem Start from what an agent tool actually *is*, mechanically, once you strip the word "AI" off it. It's a process that starts up, holds open some connections (stdio to a host, a socket, an HTTP listener), waits for requests, fans each one out to a few concurrent things — a model call, a tool call, a file read, a subprocess — collects the results, and writes a response. It does that for a long time without being restarted. It needs to be installed by people who do not have, and do not want, your development environment. Describe that to a backend engineer without ever saying "LLM" and they will tell you the language: it's a network daemon, and Go is a language designed, deliberately and narrowly, to write network daemons. **It ships as one file.** This is the single biggest reason and it has nothing to do with AI. The Go FAQ states it plainly: "The linker in the `gc` toolchain creates statically-linked binaries by default. All Go binaries therefore include the Go runtime" ([go.dev/doc/faq](https://go.dev/doc/faq)). A Go MCP server is one executable. No `pip install` that resolves differently on the user's machine than yours. No "works on my Python 3.11, your 3.13 broke a transitive dep." No asking a user to manage a virtualenv to run your tool. You cross-compile it for three platforms in CI and the install instruction is "download this file." For a tool whose entire job is to be installed into *other people's* agent setups, that property is worth more than any language feature. It's why Ollama feels like a native app and most Python AI tooling feels like a science experiment you have to host. **Goroutines are the right concurrency primitive for fan-out.** An agent step is a fan-out: call the model, and while that's in flight maybe pre-warm a tool, read a file, hit a cache. Go's answer to "do these concurrently" is `go f()` and a channel, and the runtime cost is genuinely low. From the same FAQ: goroutines "can be very cheap: they have little overhead beyond the memory for the stack, which is just a few kilobytes," the CPU overhead "averages about three cheap instructions per function call," and "it is practical to create hundreds of thousands of goroutines in the same address space" ([go.dev/doc/faq](https://go.dev/doc/faq)). You do not need an async framework, an event-loop mental model, or function coloring. A goroutine per in-flight tool call, a `context.Context` for cancellation when the model returns early, done. (It is worth knowing where this model *stops* helping you — a panic in one goroutine is not isolated the way people assume; I went deep on exactly that failure mode in [Elixir's concurrency model](https://sublimecoding.com/blog/elixir-concurrency-model), and it applies directly to long-lived agent daemons.) **The standard library already has the daemon parts.** `net/http` is a production HTTP server and client in the stdlib. `encoding/json` is in the stdlib. `os/exec` for spawning the tool subprocesses agents love is in the stdlib. `context` for deadline and cancellation propagation is in the stdlib. The dependency footprint of a competent MCP server in Go can be *zero third-party packages*, which means the supply-chain surface of the thing you're injecting into a user's machine is the Go team plus you. Compare that to the transitive dependency tree of an equivalent Python or Node tool. For software whose threat model includes "runs with access to a developer's filesystem and shell," a near-empty `go.mod` is not austerity; it's a security property. **Cold start and steady-state are both cheap.** A statically linked Go binary has no interpreter to boot and no JIT to warm. It starts, it serves, its memory is roughly what its working set is. This matters for the specific way agent tools get used: spawned per-session, sometimes per-invocation, by a host process that may start and stop them constantly. A tool that takes 800ms to import its dependency tree before it can answer the first request is a tool that makes the whole agent feel slow. (Treat the comparison as directional, not a benchmark — "no interpreter boot, no JIT warmup" is an architectural fact; the millisecond figure depends entirely on your dependency tree.) None of these four is an AI capability. That's the point. Go didn't win agent infrastructure by being good at the AI part. It won by being unreasonably good at the boring 90% of an agent tool that isn't the AI part. ## The MCP angle: proof in ~30 lines The cleanest demonstration is the Model Context Protocol, because an MCP server is the agent-tool shape distilled to its essence: read framed JSON-RPC requests on stdin, do something, write JSON-RPC responses on stdout, live as a subprocess of the host. Here is a working line-delimited MCP-style stdio server in Go that exposes one tool, with **zero imports outside the standard library**. It compiles to one binary. `package main import ( "bufio" "encoding/json" "os" "strings" ) type rpc struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id,omitempty"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result any `json:"result,omitempty"` } func main() { in := bufio.NewScanner(os.Stdin) in.Buffer(make([]byte, 1<<20), 1<<20) out := json.NewEncoder(os.Stdout) for in.Scan() { var req rpc if json.Unmarshal(in.Bytes(), &req) != nil { continue } resp := rpc{JSONRPC: "2.0", ID: req.ID} switch req.Method { case "initialize": resp.Result = map[string]any{ "protocolVersion": "2025-06-18", "capabilities": map[string]any{"tools": map[string]any{}}, "serverInfo": map[string]any{"name": "echo", "version": "0.1.0"}, } case "tools/list": resp.Result = map[string]any{"tools": []any{map[string]any{ "name": "shout", "description": "Uppercases its input.", "inputSchema": map[string]any{"type": "object"}, }}} case "tools/call": var p struct{ Arguments struct{ Text string } } _ = json.Unmarshal(req.Params, &p) resp.Result = map[string]any{"content": []any{map[string]any{ "type": "text", "text": strings.ToUpper(p.Arguments.Text), }}} default: continue // notifications and unknown methods: no reply } _ = out.Encode(&resp) } } ``` I am being deliberately honest about what this is: a *shape* demonstration, not a spec-complete server — a production MCP server adds Content-Length framing, proper error objects, schema validation, and the rest of the lifecycle, which is precisely what an SDK like mark3labs/mcp-go` or the official `modelcontextprotocol/go-sdk` gives you so you don't write it by hand. But look at what the 40 lines already are: a long-lived process, reading framed requests, dispatching concurrently if you wanted (`go handle(req)`), zero dependencies, one `go build` away from a binary you hand someone. That is the entire job. The reason every MCP SDK has a first-class Go implementation is that this is the language where that job is a Tuesday. The same daemon written to be spec-complete in Python is more code *and* a dependency tree *and* a runtime your user has to already have. If you want to see what this looks like when it's not a toy — many tools, real lifecycle, orchestrated — that's essentially the architecture I pulled apart in the [ruflo / claude-flow multi-agent deep-dive](https://sublimecoding.com/blog/ruflo-claude-flow-multi-agent-deep-dive): a swarm of these daemons is still, underneath, this loop. ## What it costs you Here is the part the title doesn't say and most "Go for AI" posts skip, because it's the part that bites you three weeks in. **The LLM-orchestration layer is verbose and joyless in Go.** The work of an agent's *brain* — assemble a prompt from fragments, call a model, parse a structured response, branch on it, maybe retry with a tweaked prompt, thread some state through — is exactly the kind of code Go is worst at. It's data-shuffling glue, and Go's error-handling model means every one of those steps is three lines (`x, err := ...; if err != nil { return ... }`) where Python is one. A prompt pipeline that is fifteen readable lines of Python becomes sixty lines of Go where the *logic* is buried under ceremony. None of those lines are wrong. They're just noise drowning the part you actually want to iterate on. **Generics help, but the ergonomics for prompt pipelines are still weak.** Go only got generics in 1.18 — the announcement calls it "our biggest change ever to the language," released 15 March 2022 ([go.dev/blog/go1.18](https://go.dev/blog/go1.18)). They're real and they help, but they're constrained by design: no sum types, no rich pattern matching, no ergonomic "this is one of these five structured outputs" the way you'd model an LLM's response variants in a language with proper ADTs. Modeling "the model returned either a tool call, or text, or a refusal, or a malformed blob" is clean in TypeScript's discriminated unions and clumsy in Go's type switches and interface assertions. The reasoning layer is *full* of that shape of problem. **JSON ⇄ struct friction is constant, and LLM output is the worst case for it.** Go's `encoding/json` wants to marshal into known, typed structs. LLM output is semi-structured, frequently almost-but-not-quite the schema, and routinely needs "parse what you can, tolerate the rest." Go's typed unmarshalling fights you here: you end up reaching for `map[string]any` and type-asserting your way through a blob, which is exactly the dynamically-typed code Go is trying to prevent you from writing — except now it's verbose dynamically-typed code. Python's "it's a dict, deal with it" is genuinely better for the messy boundary where model output meets program. **There's no official REPL, so prompt iteration has no inner loop.** Iterating on a prompt is inherently interactive: tweak wording, run it, look at the output, tweak again, ten times in two minutes. Python and a notebook are *built* for that loop. Go's edit-compile-run cycle is fast by compiled-language standards but it is not a REPL, and there is no official one — your prompt-tuning loop is "edit file, `go run`, read stdout, repeat," which is enough slower per iteration that you simply iterate less, which makes your prompts worse. This is a real product cost hiding inside a developer-experience complaint. Notice these four costs all live in the same place: the *reasoning* layer, not the daemon. Go's weaknesses are precisely Python's strengths and vice versa, which is why mature teams stop trying to pick one. ## The split that's actually emerging The interesting thing isn't "Go won" or "Python won." It's that the production answer is increasingly *both, with a wire between them*, and the seam is falling in a consistent place. The pattern: **Go owns the daemon, Python/TS owns the reasoning.** The long-lived process — the MCP server, the orchestrator, the thing holding connections and fanning out concurrent tool calls and shipping as one binary — is Go, for every reason in the "why Go fits" section. The model-facing logic — prompt construction, the eval harness, anything you want to iterate on interactively, anything that benefits from the ML ecosystem — is Python or TypeScript, called *across a boundary*: a subprocess the Go daemon spawns, a local HTTP call, an MCP tool that is itself implemented in Python. The Go process doesn't do the thinking; it's the supervisor and the I/O multiplexer for the things that do. You can see this directly in the ecosystem: the *infrastructure* repos are Go, and they call out to model code rather than embedding it. This is a better factoring than "rewrite the prompt logic in Go for consistency," which teams try once and regret. The boundary is load-bearing: it's the line between the part that has to be operationally boring (one binary, cheap concurrency, tiny dependency surface) and the part that has to be iteration-friendly (interactive, dynamically typed, ML-ecosystem-adjacent). Fighting that boundary by forcing one language across it costs you either the daemon's deployability or the reasoning layer's iteration speed. ## Verdict: when to write your agent tool in Go The honest decision rule, not the language-war version: Your situation Write it in Why MCP server, CLI tool, orchestrator, anything users *install* Go One binary, near-zero deps, cheap concurrency, ops-boring — its home turf Prompt logic, eval harness, anything you iterate on interactively Python / TS REPL/notebook loop, ergonomic semi-structured data, ML ecosystem A tool that is mostly daemon with a thin model call Go The model call is one HTTP request; don't move the daemon for it A tool that is mostly reasoning with a thin I/O wrapper Python / TS Don't pay Go's orchestration tax to save a few MB of binary A system that is meaningfully both Both, split at the daemon/reasoning seam Go supervises and multiplexes; Python/TS thinks; talk over a wire "We want one language for consistency" Pick by the *dominant* axis above Consistency is not a strong enough reason to eat the wrong side's tax The reason every AI agent framework is written in Go is that "agent framework" is a misleading name. The framework part — the part that's actually a framework — is a concurrent network daemon, and Go is the best mainstream language for concurrent network daemons by a wide margin. The AI part isn't in the framework. It's in the model, and in the prompt logic you should keep in a language built for iterating on it. Use Go for the thing Go is for. Ship the binary. Just don't let "all the infra is Go" talk you into writing your prompt pipeline there too — that's how you end up with sixty joyless lines doing fifteen lines of thinking, and a prompt you tuned three times instead of thirty because the loop was too slow to bother. --- ## Ruby Isn't Dead, It Got Boring — And Boring Is Why It Ships URL: https://sublimecoding.com/blog/ruby-boring-ships Published: 2026-05-16 Tags: ruby, developer-workflows, engineering > **TL;DR:** "Ruby is dead" is a category error. It confuses *hype velocity* — how often a language trends — with *shipping velocity* — how fast a team gets a working product in front of users and keeps it running. Those are different metrics and they are not even correlated. Meanwhile Rails 8 (November 2024) quietly deleted the three things people actually left Ruby over: the Redis dependency, the Sidekiq-style job stack, and the Node build step. The reasons to leave got removed while nobody was tweeting about it. Boring is not the absence of progress. For a team trying to launch, boring *is* the progress. Here's the honest version — including the parts Ruby still loses on. ## The obituary runs every year. The shipping never stops. There is a genre of blog post that publishes itself annually. "Is Ruby dead?" "The decline of Rails." "Why we moved off Ruby." It has run, in some form, every year for over a decade. It is one of the most reliably recurring pieces of content in our industry, which is itself the tell: you do not need to keep re-announcing the death of something that is actually dead. Nobody writes "is COBOL dead" think-pieces, because nobody's emotionally invested in the answer. The obituary cycle is a sign of *relevance*, not decline — you only keep eulogizing the thing people are still using. And people are still using it. The companies you interact with daily — the largest code-hosting platform on earth, one of the biggest commerce platforms in the world, the company that *invented* the framework and still ships consumer products on it — run Ruby in production at enormous scale, today, in 2026, having had every resource and every reason to rewrite if Ruby were actually the liability the obituaries claim. They didn't. That's not nostalgia. Rewrites are expensive and these are companies that count. They stayed because the thing still ships. So there's a contradiction to resolve. If Ruby is dead, why does the death keep needing to be announced, and why do the people with the most money and the most users keep shipping on the corpse? The resolution is that "dead" is measuring the wrong thing. ## Define "dead" properly before you use it When someone says a language is dead, ask them what metric moved. They almost always mean one of these, and they almost never mean the one that matters. **Mindshare is not usefulness.** Most "Ruby is dead" arguments are, underneath, "Ruby doesn't *trend* anymore." Fewer conference keynotes, fewer hot takes, fewer "I rewrote my side project in it" posts. All true. Also irrelevant to whether it ships software. Mindshare measures how *interesting* a technology is to talk about, and interesting-to-talk-about peaks early — when a thing is new, unsettled, and everyone's still arguing about how to use it. A mature tool stops generating novelty content precisely *because* the arguments got settled and it just works. Falling mindshare is what success looks like from the outside. It is indistinguishable, on a graph, from death — and people keep reading the graph wrong. **Hype velocity vs. shipping velocity.** Here's the distinction the whole obituary genre misses. *Hype velocity* is how fast a technology accumulates attention, stars, and conference talks. *Shipping velocity* is how fast a team using it gets a correct product in front of real users and keeps it up at 3am. These are different axes. They are arguably *inversely* related past a point: the technologies with the highest hype velocity are usually the youngest, which means the least settled, which means you spend shipping time relearning the stack every six months instead of building the product. Ruby has near-zero hype velocity in 2026 and that says nothing — *nothing* — about its shipping velocity, which is the only number a team trying to launch should care about. **The metrics that actually matter.** If you want to know whether a stack is alive in the only sense that pays rent, don't count tweets. Ask: Is it maintained and releasing? (Ruby 3.x and Rails 8 — yes, actively.) Can you hire for it? (Yes, and we'll get to the honest version of that.) Can a small team get to production fast and stay there? (This is the whole rest of the post.) Are serious companies betting revenue on it *now*, not in 2014? (Yes.) Every one of those is green. The only red metric is "does Hacker News find it exciting," and that has never once shipped a feature. This is, deliberately, the same move as [the Elixir argument](https://sublimecoding.com/blog/elixir-concurrency-model): judge the runtime by what it does at 3am, not by how it demos. Elixir is the *interesting* answer to that question. Ruby is the *boring* one. Both ship. This post is about why boring is underrated. ## What Rails 8 actually changed (tested, not asserted) Here is the part that makes the 2026 obituary not just wrong but *out of date*: the concrete, technical reasons people gave for leaving Ruby have been removed from the box. Not mitigated — removed. This is from the official Rails 8.0 release, finalized November 7, 2024 ([rubyonrails.org](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required), [release notes](https://guides.rubyonrails.org/8_0_release_notes.html)), and I'm quoting it rather than characterizing it because the specifics are the argument. **The Redis dependency is gone.** For years, "a real Rails app needs Redis" was true and was a legitimate complaint: another service to run, monitor, secure, and pay for. Rails 8 ships what the announcement calls "a trifecta of new database-backed adapters named Solid Cable, Solid Cache, and Solid Queue" — and they're database-backed, capable of running on SQLite. The release notes are explicit, per adapter: Solid Queue "replaces the need for Redis, also a separate job-running framework, like Resque, Delayed Job, or Sidekiq." Solid Cache "replaces either Redis or Memcached for storing HTML fragment caches." Solid Cable "replaces Redis to act as the pubsub server to relay WebSocket messages." That is the entire historical "Rails needs a pile of infrastructure" objection, addressed at the framework default level. The job framework *and* the cache server *and* the pubsub server collapse into "the database you already have." **The Node build step is gone.** The other durable complaint — "I came to Rails to escape JavaScript tooling and now there's a `node_modules` and a bundler in my Ruby app" — is also addressed. Per the announcement, "Propshaft is a dividend from the mission to focus on #NOBUILD as the default path" and is "now the default for all Rails 8 applications," replacing Sprockets. The default path to ship JavaScript no longer routes through a Node build pipeline. The thing people specifically left to get away from is no longer in the default stack. **Deployment stopped requiring a PaaS.** Rails 8 "comes preconfigured with Kamal 2 for deploying your application anywhere," and the generated Dockerfile "has been upgraded to include a new proxy called Thruster, which sits in front of the Puma web server." The implicit "...and you'll need Heroku to make any of this manageable" is gone too: the default new app is a container you can deploy to a plain server. Sit with the timeline. The technical case for "we left Ruby because of Redis / Sidekiq / the Node step / the PaaS tax" was *answered at the framework's default level in late 2024* — and the 2026 obituaries are still citing the pre-2024 version of the stack. They're not wrong about why people left. They're wrong that the reasons still exist. ## The boring advantage is a real advantage "Boring" gets used as an insult. It is, for a team trying to launch, a feature you should be willing to pay for. Here is the mechanism, not the platitude. **One framework decision instead of forty.** Start a project on a high-hype stack and the framework is the first of forty decisions: router, ORM, migration tool, job queue, cache layer, auth, asset pipeline, test framework, the wiring between all of them. Each is a fork in the road, a comparison matrix, a "well it depends," and a thing your next hire has to learn *because you chose it specifically*. Rails' actual product is that it has already made those forty decisions, they cohere, and they're the same forty in every Rails app on earth. That is what "omakase" means and it is worth real money: the decisions you don't make are the schedule you don't lose, and the onboarding you don't have to write. Boring here means *every Rails app looks like every other Rails app*, which is exactly the property you want when a new engineer has to be productive in week one instead of month two. **Fewer moving parts to debug at 3am.** This is the one that connects straight back to the Elixir post's frame. An incident's difficulty scales with the number of independent components that could be the cause. The pre-Rails-8 stack — app, database, Redis, Sidekiq, a Node-built asset bundle, a PaaS abstraction — is six things that can independently fail and three network boundaries between them where a 3am page can hide. The Rails 8 default stack collapses several of those *into the database you were already running and already monitoring*. Fewer services is fewer failure domains is fewer things to check while you're half awake. Boring infrastructure is infrastructure with a small incident surface, and a small incident surface is the single most underrated property a young company can have. **Settled means the answers are findable.** A boring stack's other dividend: every problem you will hit, someone hit in 2017 and wrote down. The Stack Overflow answer exists. The pattern is in a book. The framework convention tells you where the code goes so you're not relitigating project structure. High-hype stacks trade this away — the answer to your problem might be in a GitHub issue from four months ago that's already out of date because the API changed. Maturity means the search results are *correct*. That is shipping velocity, directly. None of this is exciting. That's the entire point. Excitement is a cost you pay in shipping time, and a team trying to launch should be extremely suspicious of how exciting its stack is. ## The honest limits A flagship post that only sells the upside is a brochure, so here is where Ruby genuinely loses, with no hedging — the same discipline I held the [Elixir post](https://sublimecoding.com/blog/elixir-concurrency-model) to. **Hiring optics are real, even when the hiring is fine.** You *can* hire Ruby engineers; the talent pool is deep and experienced precisely because the language is mature. But "we're a Rails shop" reads as less exciting to *some* candidates than a trendier stack, and perception is a real recruiting input even when it's not a real engineering one. You'll occasionally lose a candidate to vibes. The mitigation — strong engineers mostly care that the stack ships and the team is good — is true, but pretending the optics cost is zero is dishonest. There's a sorting effect worth naming, though: the engineers most repelled by "boring" are often the ones most attracted to novelty for its own sake, and novelty-seeking is not a trait you want concentrated in the people choosing your production architecture. The filter sometimes selects *for* you. That's a consolation, not a cancellation — you still pay the optics cost; you just occasionally get paid back in judgment. **AI/ML is still Python's, full stop.** If your product's core is model training, data science, or heavy numerical work, that work is happening in Python and Ruby is not going to change that. The realistic 2026 architecture is Ruby/Rails for the product — the part that is web application, which Rails is exceptional at — calling out to Python (or a hosted model API) for the ML. Trying to do the ML *in* Ruby is fighting the ecosystem for no prize. Know which part of your system you're talking about before you pick the language for it. **Raw concurrency is still not its game.** This is the direct handoff to the other two posts. For workloads that are fundamentally about massive concurrent connections or failure-isolated parallel work, Ruby's model is not the strong choice — that's the [Elixir/BEAM argument](https://sublimecoding.com/blog/elixir-concurrency-model) for failure isolation, and the [Go argument](https://sublimecoding.com/blog/go-ai-agent-frameworks) for concurrent network daemons. The honest framing: Rails is the best tool I know for the *web application* — the CRUD, the forms, the business logic, the admin, the 80% of most products that is not a concurrency problem. When the dominant pain genuinely *is* concurrency or failure isolation, that specific service should be Elixir or Go, and that's fine — "use Rails for the product, use the right thing for the hard concurrent edge" is a stronger architecture than forcing one runtime to do both. Notice none of these limits is "it's dead." They're "here is the specific shape of work it's not best at," which is true of every tool and is the only useful form of criticism. ## Verdict: pick Rails in 2026 when… The honest checklist, not the fan version. Reach for Rails in 2026 when most of these are true: - [ ] **The product is mostly a web application** — CRUD, forms, workflows, an admin, business logic. This is the 80% case and it's exactly Rails' home turf. - [ ] **The team is small and the deadline is real.** The forty decisions Rails already made are forty you don't lose schedule to. Boring is fastest here. - [ ] **You want a small operational surface.** The Rails 8 Solid stack collapses Redis/Sidekiq/Node/PaaS into "the database you already run." Fewer failure domains, fewer 3am unknowns. - [ ] **Your concurrency needs are normal web concurrency**, not "hundreds of thousands of stateful connections that must fail independently." If they're the latter, that service is Elixir or Go — by design, not defeat. - [ ] **Your ML is an API call or a separate Python service**, not the thing Ruby itself has to do. - [ ] **You can recruit on "we ship fast and the team is good"** rather than needing stack novelty to attract people. If most of those boxes are checked, the 2026 "Ruby is dead" narrative is not describing your reality — it's describing a stack that stopped existing in November 2024 and a metric (hype) that never shipped anything. "Dead" was always a confusion of hype velocity with shipping velocity. Ruby's hype velocity is near zero and its shipping velocity is excellent, Rails 8 just deleted the last good technical reasons to leave, and the companies with the most to lose are still on it because — quietly, unexcitingly, reliably — it ships. Boring isn't the eulogy. Boring is the whole product. --- ## 15 Elixir Libraries I Reach For in 2026 URL: https://sublimecoding.com/blog/elixir-libraries-i-reach-for-2026 Published: 2026-06-23 Tags: Elixir, AI tools, developer workflows > **TL;DR:** A curated day-one stack beats the awesome-list dump every time. The famous [awesome-elixir](https://github.com/h4cc/awesome-elixir) page is a thousand links sorted alphabetically — useful as a reference, useless as a decision. (For the other end of the spectrum, the scale-infra libraries big companies released, [I audited what Discord and Supabase open-sourced](https://sublimecoding.com/blog/discord-supabase-elixir-libraries) for whether any of it is still maintained.) This is the opposite: the ~15 packages I actually `mix deps.get` on the first morning of a new Elixir project in 2026, the one job each earns its place doing, and the libraries I dropped to get here (HTTPoison, Tesla, Quantum, hand-rolled mocks). The honest tradeoff: a tight stack means you sometimes write 20 lines you could have pulled from a dependency — and that’s the trade I take every time, because a dependency you don’t understand is a liability you can’t debug at 2am. There’s a moment on every new project where the `deps` block in `mix.exs` is empty and you have to decide who you’re inviting to the party. Get it right and the next six months are smooth. Get it wrong — pick the unmaintained thing, the over-abstracted thing, the thing with no escape hatch — and you spend those months fighting your own foundation. I’ve made enough of those mistakes to have opinions. What follows isn’t every good library; it’s the ones I reach for *by default*, before I know much about the project, because they’ve earned trust across many of them. At the end I’ll give you the rubric I use so you can judge your own stack instead of cargo-culting mine. A note on what this is *not*: it’s not a substitute for `mix phx.new`. Phoenix, Plug, Telemetry, and friends come in the box. This is about the deliberate additions on top. ## HTTP & talking to the outside world **[Req](https://hex.pm/packages/req)** — the single most consequential default I’ve changed in years. Req is a batteries-included HTTP client built on top of Finch, and it is the answer to “what do I use to make an HTTP request in Elixir?” in 2026. Retries, redirects, JSON encode/decode, compression, and streaming all work out of the box with sane defaults. I reach for it the instant a project needs to call *anything* — a payment API, an LLM endpoint, a webhook. This is the clearest “what I dropped” story in the whole list. I used to reach for HTTPoison, then Tesla. HTTPoison is effectively in maintenance mode and its ergonomics show their age. Tesla is still [actively maintained](https://hex.pm/packages/tesla) and genuinely good — its middleware model is powerful — but for most projects Req gives me 95% of what Tesla’s middleware stack did with a fraction of the ceremony. I keep Tesla in my back pocket for the rare case where I need a very specific adapter or a custom middleware pipeline shared across many clients. For everything else, Req. (José Valim maintains it; that’s not why it’s good, but it’s a decent proxy for “this will still be here in three years.”) **[Finch](https://hex.pm/packages/finch)** — I rarely write `Finch.request/2` directly anymore, because Req sits on top of it. But Finch is what I name explicitly when I need connection pooling tuned for a specific high-volume downstream — a metrics sink, an LLM provider I’m hammering. Knowing it’s the layer underneath Req is the difference between “the HTTP client is slow” and “I need a dedicated pool with 50 connections to this one host.” Reach for it directly when pool topology matters. **[Jason](https://hex.pm/packages/jason)** — JSON in and out. It’s so ubiquitous that half your other deps already pull it in. One honest caveat worth knowing in 2026: Jason’s last *stable* release predates much of this list — recent versions on Hex are alpha-tagged. In practice it’s rock-solid and battle-tested at enormous scale, so I still install it without hesitation, but it’s worth keeping an eye on whether the wider ecosystem shifts toward stdlib JSON (Elixir now ships a built-in `JSON` module) over the next year. For now, Jason stays. ## Data & Ecto **[Ecto](https://hex.pm/packages/ecto) + [ecto_sql](https://hex.pm/packages/ecto_sql)** — not optional, not interesting to argue about. If your project touches a database, Ecto is the data-mapping and query layer, full stop. What’s worth saying to newer Elixir developers: resist the urge to treat changesets as annoying boilerplate. They’re the single best place to validate input at the boundary, and leaning into them is how you keep garbage out of your domain. I write changesets even for embedded schemas that never hit a database, purely for the validation pipeline. The Ecto-adjacent thing I’ll flag: separate queries for `has_many`, JOINs for `belongs_to`. The number of N+1 problems and accidental Cartesian products I’ve reviewed because someone reached for a clever single query is not small. Ecto gives you the tools to do it right; the discipline is on you. ## Background work — the part Elixir does better than anyone **[Oban](https://hex.pm/packages/oban)** — if I could only install one library from this entire list, it would be Oban. Postgres-backed background job processing with a real database transaction guarantee, a UI, cron, uniqueness, rate limiting, and a maturity level that’s frankly rare in any ecosystem. The day-one move on any project with a database: add Oban *before* you need it, because the first time you reach for “I’ll just spawn a Task” for something that absolutely must not be lost, you’ll wish you had it. Two non-negotiables I enforce on every Oban job, no exceptions: jobs must be **idempotent** (they *will* run twice eventually), and args must use **string keys** and contain only serializable data — never a struct, never a PID. I’ve written a deeper piece on driving AI agents through Oban queues (*coming soon* — the durable-execution-for-LLM-calls pattern deserves its own post), because the retry/backoff/uniqueness machinery turns out to be exactly what flaky, expensive model calls need. **[Broadway](https://hex.pm/packages/broadway)** — not every project, but I reach for it the moment the shape is “ingest a firehose and process each message with backpressure” — SQS, Kafka, RabbitMQ, GCP Pub/Sub. Oban is for *jobs you enqueue*; Broadway is for *streams you consume*. Knowing which problem you have keeps you from bending one into the other. Skip it entirely if you don’t have a real pipeline — it’s not a default, it’s a when-you-need-it. A note on scheduling: I no longer install [Quantum](https://hex.pm/packages/quantum) for new projects. It’s a fine cron-style scheduler, but Oban Cron covers the same ground with the same durability guarantees as the rest of my background work, so I’d rather have one mental model than two. If a project already has Quantum and no Oban, that’s fine — I just don’t add it fresh. ## Web & LiveView **[Phoenix LiveView](https://hex.pm/packages/phoenix_live_view)** — the reason a one-person team can ship interactive product without a separate frontend stack. I default to LiveView for essentially all internal tools and most product UI, and only reach for a heavier client-side approach when there’s a genuine offline or ultra-low-latency requirement. The two disciplines that separate good LiveView from janky LiveView: use **streams** for any list that can grow past a hundred items (don’t hold ten thousand rows in socket assigns), and never run an unconditional DB query in `mount` — use `assign_async` or gate on `connected?/1`. I go deeper on the runtime model that makes this cheap in [Elixir’s concurrency model](https://sublimecoding.com/blog/elixir-concurrency-model) and on why the BEAM is a natural fit for [agentic workloads](https://sublimecoding.com/blog/elixir-beam-ai-agents). **[Bandit](https://hex.pm/packages/bandit)** — the pure-Elixir HTTP server that’s now the default under new Phoenix projects, replacing Cowboy. I don’t usually *add* it so much as *keep* it — but I’ll call it out because if you’re upgrading an older app still on Cowboy, moving to Bandit is one of the lower-risk, higher-clarity upgrades available: a stack trace that stays in Elixir instead of disappearing into Erlang internals is worth a lot when something goes wrong. ## Testing & developer experience **[Mox](https://hex.pm/packages/mox)** — mocks based on explicit behaviours, not magic. This is the one that enforces an architecture I’d want anyway: define a behaviour for your boundary (the payment gateway, the LLM client), mock *that*, and your production code is forced to depend on a contract rather than a concrete module. I reach for it on day one specifically because retrofitting “wrap third-party APIs behind a behaviour” later is painful. Mox is mature and stable — infrequent releases here are a feature, not neglect. The thing I dropped: hand-rolled mock modules and `:meck`-style runtime patching, both of which let you mock things that should have been refactored instead. **[ExMachina](https://hex.pm/packages/ex_machina)** — test factories. The job it earns its place doing: making test data setup readable so your tests document intent instead of drowning in struct literals. Mature, low-cadence, exactly what you want from a factory library. I’ll note the honest alternative — many teams now prefer plain factory *functions* in a test support module over a library — and that’s a legitimate choice. I still reach for ExMachina out of habit and because `build`/`insert`/`params_for` cover the cases I hit; if you’re starting fresh and want zero deps, plain functions are fine. **[StreamData](https://hex.pm/packages/stream_data)** — property-based testing. I don’t property-test everything, but for anything with a parser, a serializer, a state machine, or non-trivial data transformation, generating a thousand random valid inputs finds the edge case your three hand-written examples missed. Reach for it on the gnarly 20% of the codebase; example-based tests for the rest. **[Credo](https://hex.pm/packages/credo)** — static analysis for consistency. On a solo project it’s a second opinion; on a team it’s how you stop bikeshedding style in code review. I wire it into CI on day one so the conversation is “the linter says” rather than “I personally prefer.” **[Dialyxir](https://hex.pm/packages/dialyxir)** — the friendly Mix wrapper over Dialyzer for success typing. The honest take: Dialyzer’s error messages can be genuinely cryptic, and the first PLT build is slow. But the class of bug it catches — passing the wrong shape into a function three call-hops away — is exactly the class that’s expensive to catch any other way. I add it, I tune it to not be noisy, and I accept that it’s a long-term investment, not an instant payoff. Skip it on a throwaway prototype; add it the moment the project is going to outlive the quarter. **[Sobelow](https://hex.pm/packages/sobelow)** — security-focused static analysis built specifically for Phoenix. It catches the Phoenix-shaped footguns: missing CSRF protection, `raw/1` on untrusted content (XSS), SQL injection via string interpolation, config leaks. Runs in seconds, lives in CI, and the false-positive rate is low enough that the signal is worth the noise. Day-one install on anything with a web surface. ## The AI-era stack — where Elixir is quietly excellent This is the section that didn’t exist on my list three years ago, and it’s the reason this post anchors the site’s Elixir-and-AI cluster. The short version: the BEAM is a genuinely good place to run AI workloads — concurrent, fault-tolerant, with first-class streaming — and I’ve argued separately that [Elixir is one of the languages AI writes best](https://sublimecoding.com/blog/elixir-ai-codes-best). Here’s what I install. **[Nx](https://hex.pm/packages/nx)** — multi-dimensional tensors and numerical computing, the foundation of the entire Elixir ML stack. You rarely use it directly in a product, but it’s the layer everything else stands on, and when you need to do math on embeddings — cosine similarity, normalization — this is where it lives. **[Bumblebee](https://hex.pm/packages/bumblebee)** — pre-trained transformer models (embeddings, Whisper, BERT-family, and more) running *in your BEAM process*, no Python sidecar. The single highest-leverage use I reach for: generating embeddings locally for search and RAG instead of paying a per-token API for every chunk. It’s a young library (downloads are modest), so I treat it as “excellent for the embedding/inference cases it covers” rather than “drop-in for any model on HuggingFace.” Verify your specific model is supported before betting on it. **[Instructor](https://hex.pm/packages/instructor)** — structured, schema-validated output from LLMs. You define an Ecto schema, the library coaxes the model into returning data that conforms to it, and validation failures get fed back to the model to retry. The honest status: it’s [early-stage](https://hex.pm/packages/instructor) with low adoption and an infrequent release cadence, so I wrap it behind my own boundary module (so I can swap the implementation) rather than letting it leak through the codebase. But the *pattern* — “make the LLM return validated structs, not strings I have to parse” — is non-negotiable for any serious LLM feature, and Instructor is the cleanest expression of it in Elixir today. If you’d rather not take the dependency, the pattern is reproducible in ~50 lines with Req + Ecto changesets + a retry loop. **[pgvector](https://hex.pm/packages/pgvector)** — vector columns and nearest-neighbor search wired into Ecto. This is my honest recommendation for RAG in Elixir, and it’s deliberately *not* a RAG framework. There are dedicated RAG libraries on Hex — `arcana` and `rag_ex` both exist and have had recent releases — but their adoption is so thin (hundreds to low-thousands of downloads) that I won’t put them under a production feature yet. What I actually build with is the boring, durable combination: **pgvector** for storage and similarity search, **Bumblebee** or an embedding API for the vectors, and **Req** for the LLM calls — no framework in the middle. Postgres you already operate, queries you can read, an escape hatch at every layer. I’m writing a full walkthrough of this exact stack (*coming soon*); the headline is that you do not need a RAG framework to ship RAG in Elixir, and in 2026 you’re better off without one. ## Operations — the two I add before I think I need them **[Hammer](https://hex.pm/packages/hammer)** — rate limiting with pluggable backends. The day-one case: you have one external API endpoint, one expensive LLM call, one auth route that you do *not* want hammered, and Hammer gives you a clean token-bucket without standing up Redis if you don’t want to. Cheap to add, painful to retrofit under load. **[PromEx](https://hex.pm/packages/prom_ex)** — Prometheus metrics plus pre-built Grafana dashboards for the libraries you’re already running (Phoenix, Ecto, Oban, the BEAM itself). I add observability before the incident, not during it. The “before I need it” framing is the whole point: the metrics you wish you had are always the ones you didn’t instrument. **[tzdata](https://hex.pm/packages/tzdata)** — the unglamorous one. If your app does anything with time zones, you need a tz database, and `tzdata` is it. I mention it only because forgetting it produces confusing runtime errors the first time you try to shift a datetime into a named zone. Install it, configure it to auto-update, move on. ## The rubric — how to judge your own stack I didn’t pick these by popularity. Each one clears four bars, and you can apply the same bars to anything you’re tempted to add: Criterion The question I ask **Maintained** Is there a recent release, or is recency simply unnecessary because it’s *done*? (Mox being quiet is fine; a half-finished thing being quiet is not.) **Does one thing** Can I describe its job in a single sentence? Libraries that do five things are five things that can break. **Escape hatch** When it doesn’t do what I need, can I drop to the layer below — or am I trapped inside an abstraction? (Req → Finch is the model here.) **Debuggable at 2am** When it fails in production, does the stack trace make sense, and could I read the source if I had to? A dependency that fails any of these is one I’d rather replace with code I own. That’s the trade behind the whole list: I’ll write a little more myself to depend on a little less that I don’t understand. ## The verdict Layer Reach for Skip / dropped HTTP **Req** (Finch underneath) HTTPoison; Tesla unless you need its middleware JSON **Jason** (watch stdlib `JSON`) — Data **Ecto** + ecto_sql — Background jobs **Oban** Quantum (Oban Cron covers it) Stream ingestion **Broadway** *(when you have a pipeline)* — Web UI **Phoenix LiveView** + **Bandit** Heavy SPA unless truly needed Testing **Mox**, **ExMachina**, **StreamData** hand-rolled mocks; `:meck` Static analysis **Credo**, **Dialyxir**, **Sobelow** — AI / ML **Nx**, **Bumblebee**, **Instructor**, **pgvector** dedicated RAG frameworks (too early) Ops **Hammer**, **PromEx**, **tzdata** — Fifteen-ish names, but really one principle: a small stack of well-understood, single-purpose libraries with escape hatches beats a sprawling one you can’t fully reason about. Steal the ones that fit your project, run them through the rubric, and drop anything that fails it — including, if it fails for you, anything on this list. If you’re building on the BEAM for AI specifically, start with [why the runtime fits agentic work](https://sublimecoding.com/blog/elixir-beam-ai-agents), then [the concurrency model that makes it cheap](https://sublimecoding.com/blog/elixir-concurrency-model). The Oban-for-agents and pgvector-RAG deep dives are next in the cluster — and they’re built on exactly this stack. --- ## Build an AI Agent Loop in 50 Lines of Elixir URL: https://sublimecoding.com/blog/build-ai-agent-loop-50-lines-elixir Published: 2026-06-21 Tags: AI, agents, Elixir > **TL;DR:** Claude Code, Cursor, Devin, Codex — they all run the same pattern under the hood. The LLM proposes an action. Your code executes it. You feed the result back. The LLM proposes the next action. Repeat until done. That’s it. In Elixir, this is a GenServer with a recursive message loop. Fifty lines. No framework, no SDK beyond an HTTP client. Once you see the pattern, you can’t unsee it — and you’ll never need to treat agent frameworks as black boxes again. ## What every agent framework is hiding from you Open the source of any AI agent framework — [LangChain](https://github.com/langchain-ai/langchain), [CrewAI](https://github.com/crewAIInc/crewAI), Anthropic’s own [Claude Code](https://github.com/anthropics/claude-code) — and strip away the plugin registries, the YAML configs, the abstract base classes, the middleware stacks. What’s left is always the same thing: `loop do response = call_llm(messages) if response.wants_to_use_a_tool? result = run_tool(response.tool_name, response.tool_args) messages = messages ++ [response, result] else break response.final_answer end end ``` That’s the agent loop. The entire intellectual content of “agentic AI” is a while loop with an LLM call inside it. The frameworks add real value on top — tool registries, memory management, multi-agent routing, streaming, token tracking — but none of those are the *core*. The core is this loop. And if you don’t understand the loop, you can’t debug the framework. You can’t reason about costs. You can’t explain why your agent got stuck in an infinite cycle or burned $40 on a task that should’ve cost $0.50. So let’s build it. ## The pattern: observe → decide → act → loop Before we write code, here’s the pattern with proper names. It’s called a [ReAct loop](https://arxiv.org/abs/2210.03629) in the literature, but you don’t need the paper. It’s four steps: - **Observe** — gather the current state. On the first turn, this is the user’s goal. On subsequent turns, it’s the result of the last tool call. - **Decide** — send the accumulated context to the LLM. It either proposes a tool call or returns a final answer. - **Act** — if it proposed a tool call, execute it. If it returned a final answer, you’re done. - **Loop** — append the tool result to the conversation and go back to step 2. That’s it. The LLM is the decision engine. Your code is the executor. The conversation history is the state. In Elixir, this maps perfectly to a GenServer. The conversation history is the process state. Each iteration is a handle_info` callback that sends itself the next `:step` message. Supervision, crash isolation, and process monitoring come free from OTP — the things agent frameworks in other languages spend thousands of lines reimplementing. ## The 50-line agent Here it is. The full module — GenServer, API call, tool dispatch, loop. Count the lines yourself. `[](#cb2-1)defmodule Agent.Loop do [](#cb2-2) use GenServer [](#cb2-3) [](#cb2-4) def run(goal, tools \\ []), do: GenServer.start_link(__MODULE__, {goal, tools}) [](#cb2-5) [](#cb2-6) @impl true [](#cb2-7) def init({goal, tools}) do [](#cb2-8) send(self(), :step) [](#cb2-9) {:ok, %{messages: [%{role: "user", content: goal}], tools: tools}} [](#cb2-10) end [](#cb2-11) [](#cb2-12) @impl true [](#cb2-13) def handle_info(:step, state) do [](#cb2-14) case call_llm(state.messages, state.tools) do [](#cb2-15) {:tool_use, name, input, assistant_msg, tool_use_id} -> [](#cb2-16) result = dispatch_tool(name, input) [](#cb2-17) [](#cb2-18) tool_msg = %{ [](#cb2-19) role: "user", [](#cb2-20) content: [%{type: "tool_result", tool_use_id: tool_use_id, content: result}] [](#cb2-21) } [](#cb2-22) [](#cb2-23) send(self(), :step) [](#cb2-24) {:noreply, %{state | messages: state.messages ++ [assistant_msg, tool_msg]}} [](#cb2-25) [](#cb2-26) {:done, answer} -> [](#cb2-27) IO.puts("\n✅ Agent finished: #{answer}") [](#cb2-28) {:stop, :normal, state} [](#cb2-29) end [](#cb2-30) end [](#cb2-31) [](#cb2-32) defp call_llm(messages, tools) do [](#cb2-33) body = %{model: "claude-sonnet-5", max_tokens: 4096, messages: messages} [](#cb2-34) body = if tools == [], do: body, else: Map.put(body, :tools, tools) [](#cb2-35) [](#cb2-36) headers = [{"x-api-key", System.get_env("ANTHROPIC_API_KEY")}, {"anthropic-version", "2023-06-01"}] [](#cb2-37) {:ok, %{status: 200, body: resp}} = Req.post("https://api.anthropic.com/v1/messages", json: body, headers: headers) [](#cb2-38) [](#cb2-39) case resp["stop_reason"] do [](#cb2-40) "tool_use" -> [](#cb2-41) tool = Enum.find(resp["content"], &(&1["type"] == "tool_use")) [](#cb2-42) assistant_msg = %{role: "assistant", content: resp["content"]} [](#cb2-43) {:tool_use, tool["name"], tool["input"], assistant_msg, tool["id"]} [](#cb2-44) [](#cb2-45) _ -> [](#cb2-46) text = resp["content"] |> Enum.find(&(&1["type"] == "text")) |> Map.get("text", "") [](#cb2-47) {:done, text} [](#cb2-48) end [](#cb2-49) end [](#cb2-50) [](#cb2-51) defp dispatch_tool(name, input) do [](#cb2-52) IO.puts("🔧 #{name}(#{inspect(input)})") [](#cb2-53) [](#cb2-54) case name do [](#cb2-55) "read_file" -> input["path"] |> File.read() |> then(fn {:ok, c} -> c; {:error, r} -> "Error: #{r}" end) [](#cb2-56) "list_files" -> (input["path"] || ".") |> File.ls!() |> Enum.join("\n") [](#cb2-57) "write_file" -> File.write!(input["path"], input["content"]); "ok" [](#cb2-58) _ -> "Unknown tool: #{name}" [](#cb2-59) end [](#cb2-60) end [](#cb2-61)end ``` That’s it. No framework. No agent SDK. The init` callback sets up the conversation with the user’s goal and sends the first `:step` message. Each `:step` calls the LLM, checks whether it wants to use a tool, and either dispatches the tool and loops or prints the final answer and stops. Two things to notice about the message format. The assistant message (`%{role: "assistant", content: resp["content"]}`) carries the raw content blocks from the API response — including both text and tool_use blocks. The `tool_use_id` is passed separately and only used to build the `tool_result` message back. This keeps the messages clean: everything sent back to the API on the next turn matches the [Messages API schema](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview) exactly. ## Running it To see this work, define your tools using Anthropic’s tool use schema and call `run/2`: `[](#cb3-1)tools = [ [](#cb3-2) %{ [](#cb3-3) name: "read_file", [](#cb3-4) description: "Read the contents of a file at the given path.", [](#cb3-5) input_schema: %{ [](#cb3-6) type: "object", [](#cb3-7) properties: %{ [](#cb3-8) path: %{type: "string", description: "Absolute or relative file path"} [](#cb3-9) }, [](#cb3-10) required: ["path"] [](#cb3-11) } [](#cb3-12) }, [](#cb3-13) %{ [](#cb3-14) name: "list_files", [](#cb3-15) description: "List all files in a directory.", [](#cb3-16) input_schema: %{ [](#cb3-17) type: "object", [](#cb3-18) properties: %{ [](#cb3-19) path: %{type: "string", description: "Directory path. Defaults to current directory."} [](#cb3-20) }, [](#cb3-21) required: [] [](#cb3-22) } [](#cb3-23) } [](#cb3-24)] [](#cb3-25) [](#cb3-26)Agent.Loop.run("Read my mix.exs and tell me what Elixir version this project uses.", tools) ``` You’ll see output like: 🔧 read_file(%{"path" => "mix.exs"}) ✅ Agent finished: Based on your mix.exs, this project uses Elixir ~> 1.18. ``` The agent read the file, interpreted it, and answered — two turns, one tool call, done. For a more complex goal, it’ll chain multiple tool calls automatically. Ask it to “find all test files and summarize what they test” and you’ll watch it call list_files`, then `read_file` on each result, then synthesize. The loop just keeps going until the model is satisfied. ## What you get for free from OTP If you’ve built agent loops in Python or TypeScript, you’ve probably added code for: - **Crash isolation** — if a tool call throws, the agent shouldn’t crash your whole application. - **Supervision** — if the agent process dies, something should notice and optionally restart it. - **Concurrency** — running multiple agents simultaneously without blocking. - **Process monitoring** — knowing when an agent finishes or dies. In Elixir, you have all of these before you write a single line. The `GenServer` is a process. If it crashes, only that process dies. Wrap it in a `Supervisor` and it restarts automatically. Start ten agents and they run concurrently on the BEAM scheduler — no threads, no async/await, no event loop. Call `Process.monitor/1` on the agent’s PID to get a message when it finishes. This is what I mean when I say [the BEAM is the runtime AI agents want](https://sublimecoding.com/blog/elixir-beam-ai-agents). The primitives that agent frameworks bolt on top of Python — Celery for durability, threading for concurrency, signal handlers for cleanup — are the default in OTP. You start with them. ## The three guardrails that separate a toy from a tool The 50-line version works, but it’ll happily loop forever if the LLM gets confused, burn through your API budget on a runaway task, or hang indefinitely on a slow model call. Here are the three guardrails that make it safe for real use. ### 1. Max iterations The simplest protection: a counter. `[](#cb5-1)def init({goal, tools}) do [](#cb5-2) send(self(), :step) [](#cb5-3) [](#cb5-4) {:ok, %{ [](#cb5-5) messages: [%{role: "user", content: goal}], [](#cb5-6) tools: tools, [](#cb5-7) step: 0, [](#cb5-8) max_steps: 30 [](#cb5-9) }} [](#cb5-10)end [](#cb5-11) [](#cb5-12)def handle_info(:step, %{step: step, max_steps: max} = state) when step >= max do [](#cb5-13) IO.puts("⛔ Agent hit step limit (#{max}). Stopping.") [](#cb5-14) {:stop, :normal, state} [](#cb5-15)end [](#cb5-16) [](#cb5-17)def handle_info(:step, state) do [](#cb5-18) # ... same loop logic, plus: [](#cb5-19) {:noreply, %{state | step: state.step + 1, messages: updated_messages}} [](#cb5-20)end ``` Thirty steps is generous for most tasks. A runaway agent hitting this limit is a signal that the goal was too vague or the tools are insufficient — both things you want to know about rather than papering over with more iterations. ### 2. Token budget Model calls cost money. Track cumulative usage and kill the run when it crosses a threshold. [](#cb6-1)defp call_llm(messages, tools) do [](#cb6-2) # ... same Req.post call ... [](#cb6-3) [](#cb6-4) usage = %{ [](#cb6-5) input: resp["usage"]["input_tokens"], [](#cb6-6) output: resp["usage"]["output_tokens"] [](#cb6-7) } [](#cb6-8) [](#cb6-9) case resp["stop_reason"] do [](#cb6-10) "tool_use" -> [](#cb6-11) tool = Enum.find(resp["content"], &(&1["type"] == "tool_use")) [](#cb6-12) assistant_msg = %{role: "assistant", content: resp["content"]} [](#cb6-13) {:tool_use, tool["name"], tool["input"], assistant_msg, tool["id"], usage} [](#cb6-14) [](#cb6-15) _ -> [](#cb6-16) text = resp["content"] |> Enum.find(&(&1["type"] == "text")) |> Map.get("text", "") [](#cb6-17) {:done, text, usage} [](#cb6-18) end [](#cb6-19)end ``` Then in handle_info`: `[](#cb7-1)def handle_info(:step, state) do [](#cb7-2) case call_llm(state.messages, state.tools) do [](#cb7-3) {:tool_use, name, input, assistant_msg, tool_use_id, usage} -> [](#cb7-4) total = state.tokens_used + usage.input + usage.output [](#cb7-5) [](#cb7-6) if total > state.token_budget do [](#cb7-7) IO.puts("💸 Token budget exhausted (#{total} / #{state.token_budget})") [](#cb7-8) {:stop, :normal, state} [](#cb7-9) else [](#cb7-10) result = dispatch_tool(name, input) [](#cb7-11) tool_msg = %{role: "user", content: [%{type: "tool_result", tool_use_id: tool_use_id, content: result}]} [](#cb7-12) send(self(), :step) [](#cb7-13) {:noreply, %{state | tokens_used: total, messages: state.messages ++ [assistant_msg, tool_msg]}} [](#cb7-14) end [](#cb7-15) [](#cb7-16) {:done, answer, _usage} -> [](#cb7-17) IO.puts("\n✅ Agent finished: #{answer}") [](#cb7-18) {:stop, :normal, state} [](#cb7-19) end [](#cb7-20)end ``` A sensible default for Sonnet is 100,000 tokens per run. That’s roughly $0.80 — enough for a substantial task, cheap enough that a runaway won’t surprise you on the bill. ### 3. Timeout LLM API calls hang sometimes. The tempting fix is Process.send_after(self(), :timeout, 60_000)` — but that won’t fire while the process is blocked inside a synchronous HTTP call. The message just sits in the mailbox until `Req.post` returns, which defeats the point. The real fix is simpler: tell Req itself to enforce the deadline. `[](#cb8-1)defp call_llm(messages, tools) do [](#cb8-2) body = %{model: "claude-sonnet-5", max_tokens: 4096, messages: messages} [](#cb8-3) body = if tools == [], do: body, else: Map.put(body, :tools, tools) [](#cb8-4) [](#cb8-5) case Req.post("https://api.anthropic.com/v1/messages", [](#cb8-6) json: body, [](#cb8-7) headers: [ [](#cb8-8) {"x-api-key", System.get_env("ANTHROPIC_API_KEY")}, [](#cb8-9) {"anthropic-version", "2023-06-01"} [](#cb8-10) ], [](#cb8-11) receive_timeout: 60_000 [](#cb8-12) ) do [](#cb8-13) {:ok, %{status: 200, body: resp}} -> [](#cb8-14) # ... parse response as before ... [](#cb8-15) [](#cb8-16) {:error, %Req.TransportError{reason: :timeout}} -> [](#cb8-17) {:error, :timeout} [](#cb8-18) [](#cb8-19) {:error, reason} -> [](#cb8-20) {:error, reason} [](#cb8-21) end [](#cb8-22)end ``` Then handle it in the loop: [](#cb9-1){:error, :timeout} -> [](#cb9-2) IO.puts("⏱️ LLM call timed out after 60s. Stopping.") [](#cb9-3) {:stop, :normal, state} ``` Sixty seconds is conservative. Most Sonnet calls return in 3–15 seconds. If you’re waiting sixty, the API is having a bad day and burning more retries won’t help. ## Where to go from here You now own the primitive. Every agent framework is this loop with more stuff on top. Here’s what that “more stuff” looks like when you’re ready: **Multi-tool registries.** The dispatch_tool` function in the example is a hard-coded `case` statement. For a real system, you’d define a behaviour — `@callback execute(map()) :: String.t()` — and register modules dynamically. Each tool becomes its own module with its own tests. **Durable runs that survive deploys.** The GenServer version loses everything if the node restarts. For long-running agents (tens of minutes, dozens of steps), you want the conversation state in Postgres, not in process memory. I wrote a full walkthrough of this pattern: [Oban as a Durable AI Agent Runtime in Elixir](https://sublimecoding.com/blog/oban-durable-ai-agent-runtime). **Streaming.** The example waits for the full response before acting. For interactive use — showing the user what the agent is thinking in real-time — you’d stream tokens back through a LiveView socket. The pattern is covered in [Streaming LLM Tokens in LiveView, the 2026 Way](https://sublimecoding.com/blog/streaming-llm-tokens-liveview-2026). **Memory and context management.** After enough tool calls, the conversation history exceeds the model’s context window. Production agents prune old tool results, summarize prior steps, or use a sliding window. This is where the frameworks genuinely earn their weight. But you don’t need any of that to start. You need the loop, three guardrails, and a goal. Fifty lines. One GenServer. The rest is iteration. --- ## I Read Oban's Source Code So You Don't Have To URL: https://sublimecoding.com/blog/oban-source-code-read Published: 2026-09-03 Tags: Elixir, engineering, agents > **TL;DR:** Oban’s README tells you it’s “reliable” and “backed by Postgres.” It doesn’t tell you that job fetching is one `UPDATE ... FOR UPDATE SKIP LOCKED` wrapped in a CTE to stop the query planner from optimizing it into something slower, that leader election is a plain `INSERT ... ON CONFLICT` upsert against a single-row table, or that the same mechanism that makes it reliable — high-frequency `UPDATE`s on one table — is exactly what fills that table with dead tuples faster than autovacuum can clean them. I read `lib/oban/engines/basic.ex`, `lib/oban/notifiers/postgres.ex`, `lib/oban/peers/database.ex`, and `lib/oban/stager.ex` in the `oban-bg/oban` repo at tag `v2.23.1` (the current stable release, tagged August 2, 2026) to write this. Every mechanism below is a function I opened, not a claim I remembered from a blog post. ## Scope note I’ve [written before about running an AI agent loop on Oban](https://sublimecoding.com/blog/oban-durable-ai-agent-runtime) — the case for using it as an agent runtime instead of hand-rolling a `GenServer`. This post is the other half: *why the mechanics hold up* under that workload, and where they don’t. It assumes you’re comfortable with [the concurrency model underneath it](https://sublimecoding.com/blog/elixir-concurrency-model) — processes, supervision, and a single Postgres primary doing more work than people expect. If you’re deciding whether to use Oban, read that one first. If you’re already running it and want to know what’s actually happening under `Oban.insert/1`, read this one. ## How does Oban fetch jobs without workers fighting over them? Oban fetches a batch of jobs with a single `UPDATE` statement that uses Postgres’s `FOR UPDATE SKIP LOCKED` row lock — any row another transaction already has locked is silently skipped rather than blocked on, so N workers polling the same queue never queue behind each other waiting for a lock. The actual query lives in `Oban.Engines.Basic.fetch_jobs/3`. It’s built with Ecto, and the shape matters more than the syntax: `[](#cb1-1)WITH subset AS ( [](#cb1-2) SELECT id FROM oban_jobs [](#cb1-3) WHERE state = 'available' [](#cb1-4) AND queue = $1 [](#cb1-5) ORDER BY priority, scheduled_at, id [](#cb1-6) LIMIT $2 [](#cb1-7) FOR UPDATE SKIP LOCKED [](#cb1-8)) [](#cb1-9)UPDATE oban_jobs [](#cb1-10)SET state = 'executing', [](#cb1-11) attempted_at = now(), [](#cb1-12) attempt = attempt + 1 [](#cb1-13)FROM subset [](#cb1-14)WHERE oban_jobs.id = subset.id [](#cb1-15) AND oban_jobs.attempt < oban_jobs.max_attempts [](#cb1-16)RETURNING oban_jobs.*; ``` Two things in the real source are easy to miss reading the docs instead of the code. First, the subset` CTE isn’t decorative — the comment directly above it in `basic.ex` explains why it exists: “the Postgres planner may choose to generate a plan that executes a nested loop over the LIMITing subquery, causing more UPDATEs than LIMIT,” so the CTE acts as an “optimization fence” forcing Postgres not to flatten the query. Without it, a clever planner could re-run the inner scan per outer row and update more jobs than the `LIMIT` requested — silently over-claiming work. Second, the whole fetch runs inside `Repo.transaction/2`, and the claim (the `UPDATE`) and the read (`RETURNING`) happen atomically — a worker either gets a job it now owns, with `attempt` already incremented, or it gets nothing. The practical consequence: two nodes running 20 workers each against the same queue never contend for the same row. Each fetch takes what’s free, skips what isn’t, and the lock is released the instant the transaction commits. That’s the whole trick — it’s a boring `UPDATE`, not a distributed consensus protocol, and that’s exactly why it scales as well as it does under a single Postgres primary. ## When does Oban still poll? Oban still polls once a second by default, through a `GenServer` called the Stager, to move `scheduled` and `retryable` jobs into `available` state — `LISTEN`/`NOTIFY` tells workers a new job exists, but Postgres has no equivalent push mechanism for “this row’s scheduled time has now arrived.” `Oban.Stager` (`lib/oban/stager.ex`) runs `Engine.stage_jobs/3` on a timer (`interval: :timer.seconds(1)` in the struct default) inside `handle_info(:stage, state)`. `stage_jobs/3` in `basic.ex` is another `UPDATE`, this one moving rows whose `scheduled_at <= now()` from `scheduled`/`retryable` into `available`. Only then does it call `notify_queues/1`, which fires `Notifier.notify(conf, :insert, payload)` — the actual `LISTEN`/`NOTIFY` push that wakes idle worker processes. So the two mechanisms are layered, not redundant: staging is time-based polling because Postgres has no `NOTIFY` for “a timestamp condition became true,” and notification is push-based because once a row is staged, telling every listening producer is cheap and instant via `pg_notify`. `Oban.Notifiers.Postgres` (`lib/oban/notifiers/postgres.ex`) implements this over a dedicated `Postgrex.SimpleConnection` that issues raw `LISTEN "channel";` statements and relays incoming notifications to registered listener PIDs — one persistent connection per node, not one per queue. Worth noting from the moduledoc directly: this notifier doesn’t work behind a transaction-mode PgBouncer, because `LISTEN` needs a session-scoped connection; Oban ships `Oban.Notifiers.PG` (distributed Erlang `:pg`, no database round-trip at all) as the alternative for that topology. The one-second stage interval is also why a scheduled job in Oban is never *exactly* on time — it fires within roughly a second of its `scheduled_at`, which is fine for retry backoff and cron-adjacent work, and the wrong tool if you need sub-second scheduling precision. ## How does a cluster of nodes agree on one leader? Oban elects a leader with a plain `INSERT ... ON CONFLICT` upsert against a single-row-per-cluster `oban_peers` table — whichever node’s insert lands first holds a lease with an expiry, and every other node’s conflicting insert just fails to claim it, so there’s no separate consensus algorithm to reason about. This matters because several plugins — the cron scheduler, the pruner, the stager’s own “am I the one that runs the global stage” decision — must run exactly once across a cluster, not once per node. `Oban.Peers.Database` (`lib/oban/peers/database.ex`) runs its own election on a timer (default 30 seconds, halved when the node currently holds leadership, so a leader re-asserts twice as often as challengers probe). The core of it is `regular_upsert/2`: `[](#cb2-1)defp regular_upsert( [](#cb2-2) %{node: node, expires_at: expires_at} [](#cb2-3) = peer_data, [](#cb2-4) state [](#cb2-5) ) do [](#cb2-6) on_conflict = [](#cb2-7) "oban_peers" [](#cb2-8) |> where([p], p.node == ^node) [](#cb2-9) |> update([p], [](#cb2-10) set: [expires_at: ^expires_at] [](#cb2-11) ) [](#cb2-12) [](#cb2-13) repo_opts = [ [](#cb2-14) conflict_target: :name, [](#cb2-15) on_conflict: on_conflict [](#cb2-16) ] [](#cb2-17) [](#cb2-18) case Repo.insert_all( [](#cb2-19) state.conf, "oban_peers", [](#cb2-20) [peer_data], repo_opts [](#cb2-21) ) do [](#cb2-22) {0, nil} -> false [](#cb2-23) {_, nil} -> true [](#cb2-24) end [](#cb2-25)end ``` conflict_target: :name` means the table has a unique constraint on the cluster name — one row per Oban instance, cluster-wide. If the insert actually adds a row, `{1, nil}` comes back and that node becomes leader. If a row already exists and belongs to a *different* node, the `on_conflict` update doesn’t fire either — look closely at its `where([p], p.node == ^node)`, where `node` is destructured from the *inserting* node’s own `peer_data`. A challenger’s update matches no row, Postgres reports zero rows affected, the `case` falls to `{0, nil} -> false`, and that node isn’t leader. Crucially, the other node’s row is never touched; if it were, a challenger would be extending the sitting leader’s lease for it. That same `WHERE` clause is what makes renewal work. When the current leader re-upserts, the clause matches its own row, `expires_at` slides forward, `{1, nil}` comes back, and it keeps leadership. No heartbeat protocol, no Raft — one unique index and one `WHERE` clause doing the work of both. Leadership has a lease, not a permanent hold: `delete_expired_peers/1` runs before every election and removes any peer row past its `expires_at`, so a leader that crashes without a clean shutdown loses leadership within one `interval` window, not indefinitely. On a graceful shutdown, `terminate/2` deletes the leader’s own row and calls `Notifier.notify(conf, :leader, %{down: ...})` so the next election happens immediately instead of waiting out the timer. ## What each mechanism costs you None of this is free. Every mechanism above leans on a specific Postgres feature, and every one of those features has a bill that shows up somewhere else. Mechanism Postgres feature it leans on What it costs you Job fetch (`fetch_jobs/3`) `FOR UPDATE SKIP LOCKED` + CTE optimization fence Every fetch is still a full `UPDATE`, so it generates a dead tuple even for a job that runs in 4ms Scheduling wake-up (`stage_jobs/3`) 1-second polling `UPDATE` (Stager) A fixed floor on scheduling precision, plus one more periodic `UPDATE` on the same hot table New-job notification (`Notifier.notify/3`) `LISTEN`/`NOTIFY` over a dedicated session connection Breaks under PgBouncer transaction/statement pooling; needs `Oban.Notifiers.PG` as a fallback there Leader election (`Peers.Database`) `INSERT ... ON CONFLICT` unique-constraint upsert A single row is a hot write path every 15–30s per node; fine at normal scale, a real bottleneck only at extreme node counts State transitions (complete/error/discard) `UPDATE` in place, never `DELETE` until pruned Every one of these row updates is a new tuple version MVCC has to keep around until `VACUUM` reclaims it ## The cost nobody puts in the getting-started guide That last row is the one that actually bites in production, and it’s the one an agent workload makes worse, not better. Every job in Oban’s `oban_jobs` table goes through several `UPDATE`s in its lifetime — claimed (fetch), completed or errored, and eventually deleted by the pruner — and Postgres’s MVCC model doesn’t overwrite a row in place. Each `UPDATE` writes a new tuple version and marks the old one dead; `VACUUM` is the only thing that reclaims that space. Oban’s own [scaling guide](https://oban.hexdocs.pm/scaling.html) says it plainly: “the MVCC system only flags rows for deletion later. Then, those rows are deleted when the auto-vacuum runs” — and warns that “the default auto vacuum settings are conservative and may fall behind on active tables.” Reliability and vacuum pressure come from the same line of code. You don’t get one without the other. This is exactly the profile an AI agent workload produces: short jobs (one LLM call, one tool call), enqueued in bursts, each one going through claim → complete in seconds, at a volume that can run into the tens of thousands per hour on a busy agent fleet. That’s high UPDATE churn on one table, which is precisely what [PlanetScale’s writeup on keeping a Postgres queue healthy](https://planetscale.com/blog/keeping-a-postgres-queue-healthy) and [Richard Yen’s piece on the consequences of using Postgres as a job queue](https://richyen.com/postgres/2026/05/04/postgres_job_queue.html) both flag as the failure mode nobody budgets for: dead tuples accumulating faster than autovacuum can clean them, tables ballooning to tens of gigabytes when the live data is a few megabytes, and — the sharper point — `SKIP LOCKED` doesn’t mean rows are invisible to a scan. Postgres still has to find each dead or locked row and check its status before moving on, so a bloated table makes the *fetch itself* slower, not just `VACUUM`. If you’re running an agent fleet that enqueues a job per LLM turn, tune this before it becomes an incident — not after the table is into the tens of gigabytes the Postgres writeups keep describing. It’s also past what your application monitoring is watching for, which is the same blind spot [instrumenting LLM calls with telemetry](https://sublimecoding.com/blog/phoenix-llm-telemetry) exists to close. Oban’s own guide gives the concrete counter-move: tune autovacuum on `oban_jobs` specifically, more aggressively than Postgres’s cluster-wide defaults — `[](#cb3-1)ALTER TABLE oban_jobs SET ( [](#cb3-2) autovacuum_vacuum_scale_factor = 0, [](#cb3-3) autovacuum_vacuum_threshold = 100 [](#cb3-4)); ``` — which tells autovacuum to trigger after roughly 100 dead tuples regardless of table size, instead of the default scale-factor-based threshold that waits for a percentage of the table to go dead first (irrelevant on a small table, dangerously slow on a large one). The Oban.Plugins.Pruner` plugin, which deletes `completed`/`cancelled`/`discarded` jobs after a configurable `max_age` (default 60 seconds), is the other half — it keeps the table itself small so each `VACUUM` pass has less to do. And at genuinely extreme volume, Oban’s docs point at table partitioning as the real fix, because “dropping tables entirely is instantaneous and leaves zero bloat” — a mechanism only available in Oban Pro, not the open-source core I read for this post. ## The takeaway from reading it instead of trusting the README Oban’s reliability isn’t magic — it’s a small set of ordinary Postgres primitives (row locks, `LISTEN`/`NOTIFY`, a unique-constraint upsert, MVCC) composed carefully, with the comments in `basic.ex` showing real awareness of how the query planner could betray you. That composition is also why the failure modes are ordinary Postgres failure modes: vacuum pressure, connection pooler incompatibility, a hot single-row table under extreme leader-election load. None of that shows up if you only read the README. It shows up the first time your agent workload runs hot enough to out-churn autovacuum, which is a Tuesday, not an edge case. Reading it end to end is also a reminder of what source code is *for*. (The next one in this series is [a Linux distribution’s AGENTS.md](https://sublimecoding.com/blog/omarchy-agents-md-source-read) — a codebase where “run the tests” means booting a VM.) The reliability model came out of following intent across files and building a picture the README never states — a human habit, and one worth keeping as [the notation around our code drifts toward readers who aren’t people](https://sublimecoding.com/blog/languages-written-for-agents-not-humans). If you’re deciding whether Oban’s operational model fits your team’s AI infrastructure — or you’ve already got one of these tables quietly growing past what your monitoring is watching for — that’s a conversation worth having before the growth curve gets ahead of you. I help AI startups get this kind of infrastructure review done as part of [fractional CTO engagements](https://sublimecoding.com/consulting). --- ## Oban as a Durable AI Agent Runtime in Elixir URL: https://sublimecoding.com/blog/oban-durable-ai-agent-runtime Published: 2026-07-02 Tags: Elixir, agents, AI > **TL;DR:** If you’re building an AI agent in Elixir — a ReAct/tool-call loop that thinks, calls a tool, observes, repeats — you do not need a bespoke agent framework or a hand-rolled `GenServer` to drive it. You need a durable job queue, and you already have the best one in the ecosystem: [Oban](https://hexdocs.pm/oban). It’s Postgres-backed, idempotent by construction, retry-with-backoff out of the box, and its jobs survive a node restart because the state lives in a table, not in process memory. Model each agent step as one Oban job that enqueues the next, and your agent run keeps its place through an LLM 429, a flaky tool call, or a deploy that rolls the node mid-loop. The honest tradeoff: Oban is the wrong tool for sub-second interactive streaming (that’s LiveView’s job) and for genuinely ephemeral single-shot calls (just call the API). It shines precisely when a run is long, multi-step, costs real money per step, and absolutely must not silently die. ## The agent that lost forty steps to a deploy Here’s the scenario that made me write this. You’ve got an agent doing something genuinely useful — say, working through a backlog of support tickets, or refactoring a module across a dozen files, or researching a topic across thirty web fetches. It’s a loop: the model proposes a tool call, your code runs the tool, you feed the result back, the model proposes the next call. Twenty, forty, sixty iterations deep. Each model turn costs money. Each tool call has side effects. You drive it with the obvious thing: a `GenServer` holding the conversation history and the loop counter in its state, recursing on `handle_info`. Then you ship a deploy. The release rolls the node. The supervisor shuts down your `GenServer`. Its state — forty steps of accumulated context, the half-finished plan, the tool results you already paid for — evaporates with the process. There’s no `terminate/2` heroics that save you here, because the work isn’t “flush a buffer,” it’s “an entire stateful computation lives only in this process’s heap.” When the node comes back, the agent is just gone. No error. No resume. You find out because a customer asks why their ticket never got answered. I’ve watched a team burn an afternoon adding “checkpointing” to exactly this design — serializing the `GenServer` state to a table every N steps, reloading on boot, reconciling partial writes. By the time they finished, they had rebuilt a worse version of a durable job queue. They’d named it `AgentRunner`. They could have named it Oban. ## An agent loop is just a job queue you haven’t named yet Strip an agent down to its control flow and look at it honestly: - **Think** — call the LLM, get back a tool call (or a final answer). - **Act** — execute the tool. This can fail, time out, or partially succeed. - **Observe** — append the result to the running context. - **Repeat** — until the model says “done” or you hit a step budget. Now look at the failure modes that actually bite in production: - The LLM returns a **429** and you need to back off and try again — without losing the run. - A tool call **fails transiently** (network blip, downstream 503) and should be retried, but a *different* failure (malformed args, a 400) should not be retried forever. - A step **partially succeeds** — the tool wrote a row, then the process died before recording that it did — and a naive retry double-writes or double-charges. - The node **restarts** mid-run and the whole thing must resume from where it left off. Every one of those is a solved problem in a durable job queue. Retry-with-backoff: solved. Distinguishing “retry this” from “give up on this”: solved. Idempotency so a retried unit doesn’t double-act: solved. Surviving a restart because state lives in Postgres, not in a process: that’s the entire point of the thing. This is the reframe: **a step of a ReAct loop maps one-to-one onto an Oban job.** Thinking and acting happen inside `perform/1`. The “repeat” is the job enqueuing its successor. The conversation state — the part that must outlive any single process — lives in a row in your own `agent_runs` table, keyed by a run ID that every job in the chain carries in its args. The queue is durable because [Oban persists every job to Postgres](https://hexdocs.pm/oban) and only marks it `completed` after `perform/1` returns successfully. Crash before that, and the job is still `executing` or `available` — it will run again. You don’t get this for free with a `GenServer`. You get it for free with Oban because durability is the substrate, not a feature you bolt on. ## The code: one agent step as a durable, idempotent Oban job Here’s a real worker. It performs **one** step of an agent loop, persists the result, and enqueues the next step. It’s open-source Oban (v2.22 at time of writing) — no Pro features — and the comments call out the durability property at each point. APIs here are checked against the [current `Oban.Worker` docs](https://oban.hexdocs.pm/Oban.Worker.html). First, the run state. This is the part that must survive a restart, so it lives in a table you own: `[](#cb1-1)defmodule MyApp.Agent.Run do [](#cb1-2) use Ecto.Schema [](#cb1-3) [](#cb1-4) schema "agent_runs" do [](#cb1-5) field :goal, :string [](#cb1-6) field :status, Ecto.Enum, values: [:running, :completed, :failed], default: :running [](#cb1-7) field :step, :integer, default: 0 [](#cb1-8) field :max_steps, :integer, default: 40 [](#cb1-9) # The full ReAct transcript: every think/act/observe turn, appended in order. [](#cb1-10) # Because it's a column, a node restart can't lose it. [](#cb1-11) field :messages, {:array, :map}, default: [] [](#cb1-12) field :result, :string [](#cb1-13) [](#cb1-14) timestamps(type: :utc_datetime_usec) [](#cb1-15) end [](#cb1-16)end ``` Now the worker — one step of the loop: [](#cb2-1)defmodule MyApp.Agent.StepWorker do [](#cb2-2) use Oban.Worker, [](#cb2-3) queue: :agents, [](#cb2-4) # An agent run is long. We want generous headroom for transient LLM/tool [](#cb2-5) # failures, but NOT infinite — a poison step must eventually give up. [](#cb2-6) max_attempts: 8, [](#cb2-7) # Idempotency at the QUEUE level: never let two jobs for the same [](#cb2-8) # (run_id, step) exist concurrently. If an enqueue is retried, or two [](#cb2-9) # producers race, Oban dedupes instead of double-running the step. [](#cb2-10) unique: [ [](#cb2-11) period: :infinity, [](#cb2-12) fields: [:worker, :args], [](#cb2-13) keys: [:run_id, :step], [](#cb2-14) # Only dedupe against jobs that haven't finished. A genuinely new [](#cb2-15) # attempt at the same step after a cancel is still allowed. [](#cb2-16) states: [:available, :scheduled, :executing, :retryable] [](#cb2-17) ] [](#cb2-18) [](#cb2-19) alias MyApp.Agent.{Run, Loop} [](#cb2-20) alias MyApp.Repo [](#cb2-21) [](#cb2-22) @impl Oban.Worker [](#cb2-23) def perform(%Oban.Job{args: %{"run_id" => run_id, "step" => step}}) do [](#cb2-24) run = Repo.get!(Run, run_id) [](#cb2-25) [](#cb2-26) cond do [](#cb2-27) run.status != :running -> [](#cb2-28) # Idempotency at the DATA level: if a retry lands after the run already [](#cb2-29) # finished (e.g. the job died AFTER committing but BEFORE Oban marked it [](#cb2-30) # completed), do nothing. Re-running must be a no-op, not a double-spend. [](#cb2-31) {:cancel, :run_already_finished} [](#cb2-32) [](#cb2-33) run.step >= run.max_steps -> [](#cb2-34) finish(run, :failed, "step budget exhausted") [](#cb2-35) [](#cb2-36) true -> [](#cb2-37) run_step(run, step) [](#cb2-38) end [](#cb2-39) end [](#cb2-40) [](#cb2-41) defp run_step(run, step) do [](#cb2-42) # THINK: one LLM call. May raise on a 429 — see classify_error/1 below, [](#cb2-43) # which decides retry-with-backoff vs. give-up. If this raises, perform/1 [](#cb2-44) # never returns :ok, so Oban leaves the job retryable and the run resumes. [](#cb2-45) case Loop.next_action(run) do [](#cb2-46) {:final, answer} -> [](#cb2-47) finish(run, :completed, answer) [](#cb2-48) [](#cb2-49) {:tool_call, call} -> [](#cb2-50) # ACT: run the tool. Wrap side effects so a retried step can detect [](#cb2-51) # "I already did this" — see the idempotency note in execute_tool/2. [](#cb2-52) observation = Loop.execute_tool(run, call) [](#cb2-53) [](#cb2-54) # OBSERVE + DURABLY ADVANCE: persist the new transcript and bump the [](#cb2-55) # step counter in ONE transaction with enqueuing the next job. If the [](#cb2-56) # node dies an instant later, either the whole advance committed (next [](#cb2-57) # job is queued) or none of it did (this job is still retryable). There [](#cb2-58) # is no torn state where we advanced but forgot to schedule the next step. [](#cb2-59) advance(run, step, call, observation) [](#cb2-60) end [](#cb2-61) end [](#cb2-62) [](#cb2-63) defp advance(run, step, call, observation) do [](#cb2-64) next_step = step + 1 [](#cb2-65) [](#cb2-66) new_messages = [](#cb2-67) run.messages ++ [](#cb2-68) [%{"role" => "assistant", "tool_call" => call}, [](#cb2-69) %{"role" => "tool", "result" => observation}] [](#cb2-70) [](#cb2-71) changeset = Ecto.Changeset.change(run, messages: new_messages, step: next_step) [](#cb2-72) [](#cb2-73) # Multi ties the state write and the next-step enqueue into one DB [](#cb2-74) # transaction. Oban.insert/4 inside a Multi is first-class — the inserted [](#cb2-75) # job only becomes visible if the whole transaction commits. [](#cb2-76) Ecto.Multi.new() [](#cb2-77) |> Ecto.Multi.update(:run, changeset) [](#cb2-78) |> Oban.insert(:next, __MODULE__.new(%{"run_id" => run.id, "step" => next_step})) [](#cb2-79) |> Repo.transaction() [](#cb2-80) |> case do [](#cb2-81) {:ok, _} -> :ok [](#cb2-82) {:error, _op, reason, _changes} -> {:error, reason} [](#cb2-83) end [](#cb2-84) end [](#cb2-85) [](#cb2-86) defp finish(run, status, text) do [](#cb2-87) # Terminal write: flip status and record the result/error text. Idempotent [](#cb2-88) # because the status guard in perform/1 makes a re-run of a finished run a no-op. [](#cb2-89) run |> Ecto.Changeset.change(%{status: status, result: text}) |> Repo.update() [](#cb2-90) :ok [](#cb2-91) end [](#cb2-92)end ``` Kick off a run by inserting the first step: [](#cb3-1){:ok, run} = [](#cb3-2) %MyApp.Agent.Run{goal: "Triage and reply to ticket #4821"} [](#cb3-3) |> Ecto.Changeset.change() [](#cb3-4) |> MyApp.Repo.insert() [](#cb3-5) [](#cb3-6)# Step 0. The `unique` opts mean enqueuing this twice is harmless — the second [](#cb3-7)# insert is deduped, not run. [](#cb3-8)%{"run_id" => run.id, "step" => 0} [](#cb3-9)|> MyApp.Agent.StepWorker.new() [](#cb3-10)|> Oban.insert() ``` The shape to notice: **each step enqueues exactly the next step, transactionally, alongside the state write.** That’s the hand-rolled durable loop. There’s no driver process holding the run together. The Postgres row *is* the run; the chain of jobs is just how it walks forward. Roll a deploy at step 23 and the in-flight job goes back to available`/`retryable`; when the node returns, Oban picks it up, re-reads the row, and continues. Nothing was lost because nothing important ever lived in a process. ## Handling the failures that are specific to LLMs A generic job queue handles “the tool 503’d.” Agents add three failure modes that need deliberate handling. **1. The 429, and backoff that respects `Retry-After`.** When the LLM rate-limits you, the right move is to back off and try again *without consuming a real failure*. Oban gives you two levers. For a clean retry that doesn’t burn an attempt the same way an error does, return `{:snooze, seconds}` — it reschedules the job for later. (Note the documented quirk: [snoozing increments `max_attempts`](https://oban.hexdocs.pm/Oban.Worker.html) so total retries are preserved, which slightly skews the default backoff curve.) For genuine errors that *should* count, return `{:error, reason}` and let `backoff/1` space out the retries: `[](#cb4-1)# In MyApp.Agent.StepWorker [](#cb4-2) [](#cb4-3)@impl Oban.Worker [](#cb4-4)# backoff/1 must return non_neg_integer() seconds. The default is exponential [](#cb4-5)# (min 15s + jitter); we override to add a floor and cap. When a tool or model [](#cb4-6)# hands us an explicit Retry-After, honor it instead of guessing. [](#cb4-7)def backoff(%Oban.Job{attempt: attempt, meta: %{"retry_after" => secs}}) [](#cb4-8) when is_integer(secs), [](#cb4-9) do: secs [](#cb4-10) [](#cb4-11)def backoff(%Oban.Job{attempt: attempt}) do [](#cb4-12) base = trunc(:math.pow(2, attempt)) * 5 [](#cb4-13) min(base, 300) + :rand.uniform(10) [](#cb4-14)end ``` And inside perform/1`, classify the failure so a retryable 429 backs off while an un-retryable 400 dies cleanly instead of churning through all eight attempts: `[](#cb5-1)defp classify_and_raise({:rate_limited, retry_after}) do [](#cb5-2) # Tell Oban to wait this long, then re-raise so the job goes retryable. [](#cb5-3) # (Stash retry_after via a meta update, or snooze — pick one; don't do both.) [](#cb5-4) {:snooze, retry_after} [](#cb5-5)end [](#cb5-6) [](#cb5-7)defp classify_and_raise({:http_error, status}) when status in [400, 422], [](#cb5-8) # A malformed request will fail identically every time. Don't retry it 8x and [](#cb5-9) # don't poison the run — cancel this step and let the run record the failure. [](#cb5-10) do: {:cancel, {:permanent_tool_error, status}} [](#cb5-11) [](#cb5-12)defp classify_and_raise({:http_error, status}) when status in 500..599, [](#cb5-13) # Transient. Return an error so backoff/1 spaces out the retry. [](#cb5-14) do: {:error, {:transient_tool_error, status}} ``` {:cancel, reason}` is the one to internalize: it stops the job *and* stops retrying. That’s exactly what you want for a poison step — a step that will fail identically on every attempt. Burning all eight `max_attempts` on a guaranteed-400 is just slow money-burning. (`:discard` does the same thing but is [deprecated in favor of `{:cancel, reason}`](https://oban.hexdocs.pm/Oban.Worker.html).) **2. Partial tool output and double-execution.** This is the subtle one. Say the tool charges a payment API or POSTs to a downstream system. The job runs the tool successfully, the side effect happens — and then the node dies *before* `perform/1` returns and before the transaction commits. Oban, correctly, will retry the job, because from its perspective the step never completed. Now you’re about to run the tool a second time. The queue can’t solve this for you, because the side effect is outside Postgres. You make the *tool* idempotent: `[](#cb6-1)defp execute_tool(run, %{"name" => "charge", "args" => args} = call) do [](#cb6-2) # Derive a stable idempotency key from the run + step. A retry produces the [](#cb6-3) # SAME key, so the payment API (or your own ledger) dedupes the second call. [](#cb6-4) # This is what makes "retried step doesn't double-charge" actually true. [](#cb6-5) idem_key = "run:#{run.id}:step:#{run.step}:charge" [](#cb6-6) PaymentAPI.charge(args, idempotency_key: idem_key) [](#cb6-7)end ``` Most serious APIs accept an idempotency key. For your own writes, enforce it with a unique constraint keyed on {run_id, step}` and treat the constraint violation as “already done.” The queue’s `unique` option stops *duplicate jobs*; the idempotency key stops *duplicate side effects from one job’s retries*. You need both, and they operate at different layers. **3. The poison run, not just the poison step.** `max_attempts` bounds a single step. Bound the whole run too — that’s the `run.step >= run.max_steps` guard in `perform/1`. An agent stuck in a tool-call loop will happily enqueue itself forever; the step budget in your own table is the circuit breaker. Cheap, and it lives in the same durable place as everything else. If your runs are complex enough to need fan-out, branches, or a real dependency graph (step C waits on A *and* B), that’s where [**Oban Pro’s `Workflow`**](https://oban.pro/docs/pro/Oban.Pro.Workflow.html) earns its license — it models jobs as a DAG and supports appending jobs to a running workflow when you don’t know all the steps up front. That’s a **paid tier** feature; the self-enqueuing chain above is plain open-source Oban and covers the linear ReAct loop, which is the common case. ## When NOT to reach for Oban here This pattern is not a hammer. Three cases where it’s the wrong call: **Sub-second interactive streaming.** If a human is watching tokens stream into a chat UI and expects a reply in two seconds, Oban is the wrong layer — full stop. Queue latency, even when small, is latency you’re adding to a tight interactive loop, and you lose token streaming entirely. That’s [**LiveView’s**](https://sublimecoding.com/blog/elixir-concurrency-model) job: stream the model output straight to the client over the socket, keep the in-flight conversation in the LiveView process, and persist when the turn completes. (A dedicated post on the LiveView-streaming side of this is coming soon.) Oban is for the *durable background* agent, not the *live foreground* chat. **Genuinely ephemeral single-shot calls.** If the whole “agent” is one prompt, one response, no tools, no loop — just call the API in a `Task` and move on. Wrapping a single stateless call in a durable job is ceremony with no payoff. The durability machinery earns its keep only when there’s multi-step state worth protecting. **An in-memory, long-lived conversation that must stay hot.** If you genuinely need a conversation held live in memory across many turns with sub-step-latency access to working state — a `GenServer` (ideally under a `DynamicSupervisor`, keyed in a `Registry`) is the right model, and that’s a legitimate use of a process: it models real concurrent, stateful, isolated work. The honest line is the one I drew at the top: reach for the `GenServer` when state must stay *hot in memory*; reach for Oban when state must stay *durable on disk and survive the process dying*. Most batch/background agents want the latter. For more on when a process is and isn’t the answer, see [the BEAM-for-agents piece](https://sublimecoding.com/blog/elixir-beam-ai-agents) and [the concurrency-model deep dive](https://sublimecoding.com/blog/elixir-concurrency-model). ## Verdict Oban (self-enqueuing chain) Plain GenServer loop Dedicated agent framework **Survives a deploy mid-run** Yes — state in Postgres, job resumes No — state dies with the process Depends; usually you provide the store **Retry + backoff on 429/tool failure** Built in (`backoff/1`, `{:snooze, _}`, `{:error, _}`) You build it Usually built in **Idempotency / no double-charge** `unique` jobs + your idem keys You build all of it Varies, often weak **Sub-second token streaming** No — use LiveView Yes (in-process) Varies **Distributed across nodes** Yes — any node pulls the job No — pinned to one process Varies **Operational visibility** Oban Web / DB queries You build it Varies **Best when** Long, multi-step, costly, must-not-die runs Hot in-memory live conversation You want the framework’s batteries and will pay the lock-in The rule I’d give a team: **if losing the run is a real cost — money already spent, side effects already committed, a customer waiting — make each step an Oban job.** You’ll get durability, retries, idempotency, backoff, and cross-node distribution from a library your Elixir app probably already runs, instead of reinventing a worse queue and calling it an agent framework. Save the `GenServer` for the genuinely live, in-memory case, and reach for [Oban Pro](https://oban.pro) only when a linear chain isn’t enough and you need real DAGs. A companion post on RAG retrieval as a set of cached, idempotent Oban jobs is coming soon. The durability argument there is the same — it just happens one layer down, at the tool. --- *Building an Elixir agent system and trying to decide where the durability boundary goes? That’s exactly the kind of call I help teams make as a fractional CTO. [Get in touch.](https://sublimecoding.com/consulting)* *Primary sources: [Oban docs](https://hexdocs.pm/oban) · [`Oban.Worker`](https://oban.hexdocs.pm/Oban.Worker.html) · [Oban on GitHub](https://github.com/oban-bg/oban) · [Oban Pro `Workflow`](https://oban.pro/docs/pro/Oban.Pro.Workflow.html).* --- ## 12 Phoenix LiveView Patterns for Production URL: https://sublimecoding.com/blog/phoenix-liveview-patterns-production Published: 2026-07-21 Tags: Elixir, Phoenix, engineering > **TL;DR:** Every pattern below is a scar. I learned each one because the absence of it took down a page, leaked a row a user shouldn’t have seen, or pinned a node’s memory until it OOM’d. This isn’t a tips list — tips are cheap and forgettable. It’s twelve named failure modes and the LiveView construct that prevents each. If you’re shipping LiveView past the tutorial stage, you’ve already hit some of these or you’re about to. The meta-pattern at the end is the one that actually matters: the boundary between the dead render and the connected socket is where most LiveView bugs live. LiveView tutorials age well — the framework is stable, the docs are excellent, and a fresh `mix phx.gen.live` gets you a working CRUD page in minutes. What the tutorials don’t give you is the second-order knowledge: the things that work fine with ten rows and fall over with ten thousand, the patterns that are obviously correct in hindsight and obviously absent in every junior PR — and in [most of what AI coding agents generate](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong). So this is the listicle I wish existed when I was three production LiveView apps in. Each item is a pattern, the specific failure it prevents, a short real snippet, and — because honesty matters more than completeness — a note on when the pattern is overkill. Verified against Phoenix LiveView 1.1. I’ve grouped them loosely: rendering and data, async, real-time, navigation and UI, and safety. Read top to bottom or jump to the one that’s currently on fire. --- ## Rendering and data ### 1. Use streams for unbounded lists — don’t hold 10k rows in assigns **The failure mode:** You `assign(:messages, all_messages)` and it works great in dev. In production the list grows to 8,000 rows, and now every LiveView process holds all 8,000 in memory, re-diffs the whole collection on every update, and ships fat payloads down the socket. A few hundred concurrent users and the node’s memory chart looks like a hockey stick. Streams keep the collection in the DOM, not in the socket’s assigns. The server holds only the IDs it needs to address. `[](#cb1-1)def mount(_params, _session, socket) do [](#cb1-2) {:ok, stream(socket, :messages, Chat.recent_messages())} [](#cb1-3)end [](#cb1-4) [](#cb1-5)def handle_info({:new_message, msg}, socket) do [](#cb1-6) # prepend, and prune the DOM to 50 items so it never grows unbounded [](#cb1-7) {:noreply, stream_insert(socket, :messages, msg, at: 0, limit: 50)} [](#cb1-8)end ``` [](#cb2-1)
| {user.name} | [](#cb8-5)
[](#cb5-13) The model dropped the connection. [](#cb5-14) [](#cb5-15)
[](#cb5-16) [](#cb5-17) [](#cb5-18){entry.client_name}
[](#cb2-6) [](#cb2-7) {entry.progress}% [](#cb2-8) [](#cb2-9)[](#cb2-10) {upload_error_to_string(err)} [](#cb2-11)
[](#cb2-12)[](#cb2-15) {upload_error_to_string(err)} [](#cb2-16)
[](#cb2-17)[](#cb5-4) ▌ Analyzing… [](#cb5-5)
[](#cb5-6) <% :rejected -> %> [](#cb5-7)File rejected before analysis.
[](#cb5-8) <% _ -> %> [](#cb5-9)<% end %> [](#cb5-10) [](#cb5-11)<.async_result :let={text} assign={@analysis}> [](#cb5-12) <:loading>Thinking… [](#cb5-13) <:failed :let={_reason}> [](#cb5-14)The model couldn't read that file.
[](#cb5-15) [](#cb5-16){text}
[](#cb5-17) ``` handle_async/3` closes the loop, same two-terminal-outcomes shape I used for [streaming LLM tokens in LiveView](https://sublimecoding.com/blog/streaming-llm-tokens-liveview-2026) — a clean `AsyncResult.ok` or a `AsyncResult.failed`, nothing left hanging: `[](#cb6-1)def handle_async(:analysis, {:ok, text}, socket) do [](#cb6-2) {:noreply, [](#cb6-3) socket [](#cb6-4) |> assign(:stage, :done) [](#cb6-5) |> assign(:analysis, [](#cb6-6) AsyncResult.ok(socket.assigns.analysis, text))} [](#cb6-7)end [](#cb6-8) [](#cb6-9)def handle_async(:analysis, {:exit, reason}, socket) do [](#cb6-10) {:noreply, [](#cb6-11) assign(socket, :analysis, [](#cb6-12) AsyncResult.failed(socket.assigns.analysis, reason))} [](#cb6-13)end ``` ## The page-count gate: the boundary check no vendor doc writes for you allow_upload`’s limits stop a file that’s the wrong *type* or the wrong *size*. Neither stops a file that’s the wrong *shape* — a PDF that’s technically 6MB but has 80 pages of dense text, which is exactly the kind of file a real user uploads to a “summarize this contract” feature. Claude’s own PDF limits cap requests at 600 pages, or 100 pages when the request’s context window is under 1M tokens — which is the default for most models you’ll actually be calling ([Anthropic PDF support, page and size limits](https://platform.claude.com/docs/en/build-with-claude/pdf-support#check-pdf-requirements)). A file under every `allow_upload` limit can still land well past that. Getting an exact page count means parsing the PDF’s object structure, which is more machinery than this feature needs. A byte-scan for page objects is a cheap, honest approximation — cheap enough to run before the API call, honest enough to catch the case that matters (an 80-page file, not an off-by-one on a 99-page one): `[](#cb7-1)defmodule MyApp.PdfInspect do [](#cb7-2) # ponytail: byte-scan heuristic, not a real parser. [](#cb7-3) # Swap for a proper PDF library if you need exact [](#cb7-4) # counts on malformed or linearized PDFs. [](#cb7-5) def count_pages(binary) when is_binary(binary) do [](#cb7-6) ~r{/Type\s*/Page[^s]} [](#cb7-7) |> Regex.scan(binary) [](#cb7-8) |> length() [](#cb7-9) end [](#cb7-10)end [](#cb7-11) [](#cb7-12)defp boundary_check(binary, "application/pdf") do [](#cb7-13) pages = MyApp.PdfInspect.count_pages(binary) [](#cb7-14) [](#cb7-15) if pages <= 100 do [](#cb7-16) :ok [](#cb7-17) else [](#cb7-18) {:error, "PDF has #{pages} pages; 100 max."} [](#cb7-19) end [](#cb7-20)end [](#cb7-21) [](#cb7-22)defp boundary_check(_binary, _media_type), do: :ok ``` This is the gate the brief for this post exists to make: the check runs on bytes already in memory (read once, inside the consume callback) but strictly before start_async` fires the network call. A rejected 200-page PDF costs one disk read and a regex scan. An accepted one costs a real API call and every token on every page. ## What the request actually looks like — image and PDF Both paths funnel through one wrapper, because a LiveView should never hold an HTTP client directly against a third-party API — same rule I used for the streaming post’s `MyApp.LLM` module. The only branch is the content-block `type`: `"image"` for pictures, `"document"` for PDFs, both carrying the same `base64` source shape: `[](#cb8-1)defmodule MyApp.ClaudeVision do [](#cb8-2) @moduledoc "Boundary around Claude's vision/PDF API." [](#cb8-3) [](#cb8-4) @endpoint "https://api.anthropic.com/v1/messages" [](#cb8-5) @model "claude-sonnet-5" [](#cb8-6) [](#cb8-7) def analyze(binary, media_type) do [](#cb8-8) block_type = [](#cb8-9) if media_type == "application/pdf", [](#cb8-10) do: "document", [](#cb8-11) else: "image" [](#cb8-12) [](#cb8-13) data = Base.encode64(binary) [](#cb8-14) [](#cb8-15) body = %{ [](#cb8-16) model: @model, [](#cb8-17) max_tokens: 1024, [](#cb8-18) messages: [ [](#cb8-19) %{ [](#cb8-20) role: "user", [](#cb8-21) content: [ [](#cb8-22) %{ [](#cb8-23) type: block_type, [](#cb8-24) source: %{ [](#cb8-25) type: "base64", [](#cb8-26) media_type: media_type, [](#cb8-27) data: data [](#cb8-28) } [](#cb8-29) }, [](#cb8-30) %{type: "text", text: prompt_for(block_type)} [](#cb8-31) ] [](#cb8-32) } [](#cb8-33) ] [](#cb8-34) } [](#cb8-35) [](#cb8-36) resp = [](#cb8-37) Req.post!(@endpoint, [](#cb8-38) headers: [ [](#cb8-39) {"x-api-key", api_key()}, [](#cb8-40) {"anthropic-version", "2023-06-01"} [](#cb8-41) ], [](#cb8-42) json: body, [](#cb8-43) receive_timeout: 60_000 [](#cb8-44) ) [](#cb8-45) [](#cb8-46) resp.body["content"] [](#cb8-47) |> Enum.map_join("", & &1["text"]) [](#cb8-48) end [](#cb8-49) [](#cb8-50) defp prompt_for("document"), [](#cb8-51) do: "Summarize this document's key points." [](#cb8-52) [](#cb8-53) defp prompt_for("image"), [](#cb8-54) do: "Describe what's in this image." [](#cb8-55) [](#cb8-56) defp api_key, do: System.fetch_env!("ANTHROPIC_API_KEY") [](#cb8-57)end ``` That request shape — type: "image"` vs. `type: "document"`, both under `source: {type: "base64", media_type: ..., data: ...}` — comes straight from Anthropic’s Messages API reference for [vision](https://platform.claude.com/docs/en/build-with-claude/vision#base64-encoded-image-example) and [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support#option-2-base64-encoded-pdf-document) respectively. Under the hood, a PDF isn’t parsed as text-only: each page is rendered to an image and paired with its extracted text, so Claude reasons over the layout, charts, and tables the same way it reasons over a screenshot ([how PDF support works](https://platform.claude.com/docs/en/build-with-claude/pdf-support#how-pdf-support-works)). That’s the meaningful difference from a RAG pipeline: [pgvector-backed RAG in Phoenix](https://sublimecoding.com/blog/phoenix-rag-pgvector-arcana) chunks and embeds *text*, blind to layout; this path reasons over the document *visually*, which is what you want for a form, a scanned contract, or a chart nobody transcribed. ## Inline base64 vs. the Files API Every example above sends the file inline as base64, which is the right default for this feature: one upload, one analysis, one request. Reach for the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) instead when either of two things is true. First, you’re near the ceiling — a PDF pushing toward the 32MB total request-size limit, or an image close to the 10MB base64 cap, where the encoding overhead itself becomes the constraint. Second, the same file gets referenced across multiple turns — a chat-style feature where the user asks three follow-up questions about the document they uploaded once. Anthropic’s own guidance is explicit here: resending base64 data on every turn of a multi-turn conversation grows your payload every turn even though the file never changed; uploading once and referencing a `file_id` keeps it flat ([Files API for images](https://platform.claude.com/docs/en/build-with-claude/vision#files-api-image-example)). For the single-shot “drop a file, get an analysis” flow this post builds, inline base64 is the simpler correct answer — don’t add the extra round trip until a real multi-turn feature earns it. ## Streaming the analysis back Everything above waits for the full `AsyncResult.ok` before rendering anything — fine for a one- or two-sentence description, worse for a long document summary where the user is staring at “Thinking…” for ten seconds with nothing to read. If you want the response to stream token-by-token instead, the plumbing doesn’t change: swap the single `AsyncResult.ok` call for Claude’s streaming response wired into the same `start_async` task via `send/2` and `handle_info/2`. That’s the exact pattern — cancellation-safe, backpressure-aware — I built out in full in [Streaming LLM Tokens in LiveView, the 2026 Way](https://sublimecoding.com/blog/streaming-llm-tokens-liveview-2026); nothing about the upload, the boundary check, or the process lifecycle here changes when you add it. ## What this actually costs The page-count gate above isn’t paranoia — vision requests are priced in visual tokens, and they add up faster than text. Claude tiles an image into 28×28-pixel patches, so cost is `⌈width / 28⌉ × ⌈height / 28⌉` tokens: a 1000×1000px image runs 1,296 tokens on any tier, and models with high-resolution support (Sonnet 5 and Opus 4.8 among them) can process up to a 2576px long edge at roughly 4,784 tokens before downscaling caps it further ([resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#resolution-and-token-cost)). A single screenshot is cheap. A batch of them adds up exactly like any other per-token cost. PDFs cost more per unit, because each page pays twice: roughly 1,500–3,000 text tokens per page for the extracted content, plus the same image-token cost as a rendered screenshot of that page, since PDF support is built on the vision pipeline ([PDF cost estimation](https://platform.claude.com/docs/en/build-with-claude/pdf-support#estimate-your-costs)). A ten-page contract can run 20,000+ tokens before the model writes a single word back. That’s the real reason the page-count gate belongs in the request path and not in a monitoring dashboard you check after the bill arrives — same principle as validating a prompt won’t blow the context window before you ever open a socket to the model. ## Where the process boundary matters One more thing worth naming, because it’s easy to build this feature and never think about it: every piece of this — the upload, the boundary check, the Claude call, the async result — lives inside one user’s LiveView process. If it crashes mid-analysis, no other user’s upload is affected, and the `Req` call inside `start_async` dies with it, which means you’re not paying for a request nobody’s waiting on anymore. That’s not an accident of this design, it’s the reason to build vision features in Phoenix in the first place — the same per-request process isolation I laid out in [Elixir’s BEAM Is the Runtime AI Agents Want](https://sublimecoding.com/blog/elixir-beam-ai-agents). If this single-shot analysis grows into something that loops — ask a follow-up, call a tool, re-analyze with more context — the shape to reach for is the same GenServer loop from [Build an AI Agent Loop in 50 Lines of Elixir](https://sublimecoding.com/blog/build-ai-agent-loop-50-lines-elixir), not a bigger LiveView. ## When not to build it this way If the files are large, numerous, or need to be processed unattended — a nightly batch that re-analyzes every document uploaded that day — don’t run it through a live user-facing socket at all. Use Anthropic’s [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/pdf-support#process-document-batches) and an Oban job, and show the user a “processing” state that updates when the job finishes rather than holding a LiveView connection open for a long-running batch. And if the upload target is genuinely enormous — video, multi-gigabyte archives — you’re outside what any of Claude’s vision or document endpoints are built for; that’s a different pipeline entirely. For the common case — a user drops a screenshot or a short PDF and wants an answer in seconds — this is the whole shape: validate at the boundary before it costs a token, consume the entry off disk, split the upload state from the analysis state in the UI, and let the process die cleanly when either the user or the model gives up. If you’re building this kind of feature into an early-stage product and want a second set of eyes on where the cost and security boundaries actually belong, that’s exactly the kind of conversation a [fractional CTO engagement](https://sublimecoding.com/consulting) starts with. --- ## The Claude Code Resource Bible: 46 Tools Worth Knowing in 2026 URL: https://sublimecoding.com/blog/claude-code-resource-bible Published: 2026-05-07 Tags: AI tools, agents, developer workflows, mcp > **TL;DR:** 46 tools across the Claude Code ecosystem, organized by category (official, directories, MCP servers, skills, multiplexers, agent frameworks, automation), with picks for what's actually worth your time and what to skip. If you read one section, read the Recommended Starter Stack at the bottom. Claude Code went from a CLI wrapper to a real ecosystem in under a year. The reference MCP server repo has eighty-five thousand stars. There are at least five different "awesome Claude skills" lists, and the smallest one has more entries than I can remember. Half a dozen terminal multiplexers exist purely to manage parallel Claude sessions. The ecosystem is real, and most of what's in it is noise. This post is a curated map. I've installed, broken, and uninstalled most of these tools myself over the last several months. The goal is to save you the half-day of comparison-shopping that I already spent. Where I have an opinion, I'll give it. Where a tool is famous but mid, I'll say so. Where something genuinely changed how I work, it gets a Sublime pick. What this post is not: a sponsored list, a roundup of every GitHub repo with "claude" in the name, or a vibes-based ranking. If a tool isn't here, it's either because the URL didn't verify, the project looked abandoned, or I couldn't articulate why anyone should install it. There are also a handful of tools you'd expect to see based on aggregated infographics floating around X (you know the ones) that turned out to be either misnamed or non-existent. I dropped them rather than dressing up a 404. A note on count. The earlier draft of this list had fifty-five entries. After verification, ten of them either pointed at the wrong repo, didn't exist, or duplicated something now bundled into Claude Code itself. I'm not going to round up. The honest number is forty-six. ## How to use this list If you're new to Claude Code, start at the Official Anthropic section and don't skip ahead until your CLI is set up. If you've been running Claude Code for a few weeks and want integrations, jump to MCP Servers. If you're already managing multiple agents and the bottleneck is coordination, Agent Frameworks is where you'll spend your time. A quick taxonomy, because the ecosystem invents new words for old things. **MCP servers** are network-accessible tool providers. They expose a database, an API, or a service over a standard protocol so Claude can call them. **Skills** are scoped instruction packs that ride alongside the system prompt. They're roughly "how to do X well, with the right files and commands." **Plugins** are bundles that may include skills, slash commands, hooks, and MCP wiring at once. **Marketplaces** are install registries for any of the above. That's the whole conceptual model. If a tool description seems wordier than that, it's marketing. One more rule of thumb: prefer fewer, sharper integrations over a kitchen-sink install. Each MCP server costs context window and adds another thing to debug when an agent goes sideways. The temptation, especially when reading a list like this, is to install everything that looks interesting. Don't. Pick the three or four that map to systems you actually use today. You can always add more later. ## Official Anthropic Start here. If you're new, the Anthropic docs and reference servers will save you a week of misinformation collected from screenshots on X. **Claude Code Docs.** [code.claude.com/docs](https://code.claude.com/docs) is the source of truth for the CLI, slash commands, hooks, settings.json, and the plugin system. When a third-party tutorial contradicts the docs, trust the docs. **Anthropic Partner Network.** Announced via the [Partner Network post](https://www.anthropic.com/news/claude-partner-network), this is Anthropic's formal program for systems integrators and consultancies. Useful if you're trying to land enterprise work on top of Claude. Less useful day-to-day. **Claude Certified Architect.** Delivered through the Partner Portal under "Claude Certified Architect, Foundations." There's no standalone public page, so don't expect to enroll like it's AWS Cloud Practitioner. Your company has to be in the partner network. **Official MCP Servers Repo.** [github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) hosts the seven actively-maintained reference servers (Everything, Fetch, Filesystem, Git, Memory, Sequential Thinking, Time). Older reference servers (Postgres, Slack, GitHub) have been moved to [servers-archived](https://github.com/modelcontextprotocol/servers) and superseded by vendor-maintained versions. Always check the official org first before pulling a fork. > **Sublime pick:** The Official MCP Servers Repo. When you're putting MCPs in front of production code, "trustworthy provenance" is the only feature that matters. ## Directories and mega lists These are bookmark-worthy, not install-worthy. Use them to discover, then go install from the source. **Everything Claude Code (ecc.tools).** [ecc.tools](https://ecc.tools) is the product face of [affaan-m/everything-claude-code](https://ecc.tools), a community library of skills, agents, and slash commands. The site indexes 181 skills, 47 agents, and 79 commands at the time of writing. Good for browsing; quality varies. **awesome-claude-code (hesreallyhim).** [github.com/hesreallyhim/awesome-claude-code](https://github.com/hesreallyhim/awesome-claude-code) is the de-facto mega list, with 42.8k stars. It's currently mid-reorganization, so the structure may shift, but the link surface is the broadest you'll find. **Awesome MCP Servers (punkpeye).** [github.com/punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers) sits at 86.4k stars and is the canonical MCP discovery list. Categorized, well-maintained, and updated weekly. **50+ Best MCP Addons (claudefa.st).** [claudefa.st/blog/tools/mcp-extensions/best-addons](https://claudefa.st/blog/tools/mcp-extensions/best-addons) is a more editorial take, last updated yesterday relative to publication. Useful when you want a human to have already culled the duds. > **Sublime pick:** Awesome MCP Servers (punkpeye). It's the only list I open more than once a month. Bookmark it; don't try to install everything in it. ## MCP servers, the integration layer This is where Claude Code stops being a chat that writes code and starts being an agent that touches your real systems. Pick servers based on the systems you actually use, not the ones that look cool in a demo. **Playwright MCP.** [github.com/microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) is Microsoft's official browser automation server. 32.1k stars. If you want Claude to open a browser, click around, and verify the result, this is the one. The accessibility-tree mode is faster than screenshot mode for most testing. `claude mcp add playwright npx @playwright/mcp@latest ``` **Firecrawl MCP.** [github.com/firecrawl/firecrawl-mcp-server](https://github.com/firecrawl/firecrawl-mcp-server) is the official Firecrawl scraping and search server. If you've ever asked Claude to "go read this URL" and watched it hallucinate the contents, Firecrawl fixes that. You'll need a Firecrawl API key. **Supabase MCP.** [github.com/supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp) is the community-maintained server (note the supabase-community/` namespace, not `supabase/`). Apache 2.0, actively released. Frame this in your head as a community project that Supabase blesses, not a first-party product. **crystaldba/postgres-mcp.** The original modelcontextprotocol/servers Postgres reference is archived. [github.com/crystaldba/postgres-mcp](https://github.com/crystaldba/postgres-mcp) is the maintained replacement I'd grab today. It exposes schema introspection plus query tools without you having to write a custom server. **GitHub MCP.** [github.com/github/github-mcp-server](https://github.com/github/github-mcp-server) is the official GitHub server, currently at v1.0.3. PRs, issues, code search, and file operations all in one place. If you're using Claude Code in a real repo, install this immediately. **Context7.** [github.com/upstash/context7](https://github.com/upstash/context7) is Upstash's live-docs server. 54.7k stars, dozens of releases, and the only sane answer to "Claude is using a deprecated API from 2023." It pulls current library docs at request time. The single biggest unforced-error eliminator I've installed this year. **Sentry MCP.** [github.com/getsentry/sentry-mcp](https://github.com/getsentry/sentry-mcp) is the official Sentry server, also hosted at [mcp.sentry.dev](https://mcp.sentry.dev). Wire this up and Claude can pull live errors, trace events, and triage issues. For production debugging it's a step-change. **Dokploy MCP.** [github.com/Dokploy/mcp](https://github.com/Dokploy/mcp) is Dokploy's official package. If you self-host with Dokploy, this gives Claude direct access to your deployment surface. **Slack MCP.** The official reference at [npmjs.com/package/@modelcontextprotocol/server-slack](https://www.npmjs.com/package/@modelcontextprotocol/server-slack) was archived from the main MCP repo. Slack also runs an official remote server documented at [docs.slack.dev/ai/slack-mcp-server](https://docs.slack.dev/ai/slack-mcp-server). For new projects, use the remote. **Notion MCP.** [github.com/makenotion/notion-mcp-server](https://github.com/makenotion/notion-mcp-server) is the official local server, currently v2.0.0. Notion is migrating users to a remote MCP, and the local server may eventually sunset. If you're starting fresh, plan for the remote. **Figma MCP.** Setup instructions live at [help.figma.com](https://help.figma.com/hc/en-us/articles/39888612464151-Claude-Code-and-Figma-Set-up-the-MCP-server). The recommended path is the remote at `mcp.figma.com`, not a local install. For implementing a designer's mockup in code, this is the cleanest handoff I've seen. **Linear MCP.** [linear.app/docs/mcp](https://linear.app/docs/mcp) documents the official remote MCP at `https://mcp.linear.app/mcp`, OAuth 2.1. There's no public source repo; it's a hosted service. Pair it with the GitHub MCP and "ship the linear ticket" becomes one prompt. Add it like this: `claude mcp add --transport http linear https://mcp.linear.app/mcp ``` A practical note on layering MCPs: I keep my most-used four (GitHub, Context7, Sentry, Linear) in user-scope ~/.claude.json`, and project-specific ones (Playwright for a frontend repo, Postgres for a service repo) in the project's `.mcp.json`. That way I don't pay the context cost of Playwright on a Go service that has nothing to render. > **Sublime pick:** Context7. It kills the stale-docs problem instantly and you don't have to remember to mention it. Once it's installed, Claude reaches for it automatically when a library version matters. **Honorable mention:** Sentry MCP. The first time Claude pulled a live trace and proposed the fix without me copy-pasting an error, I knew this category was going to swallow my error-investigation workflow. ## Articles and blog posts worth reading Quick annotations, not deep dives. [10 CLI Tools for AI Coding (Starmorph)](https://blog.starmorph.com/blog/10-cli-tools-for-ai-coding) by Dylan Boudro. Solid intro to the broader CLI agent space if you're trying to compare Claude Code to Codex, Aider, and friends. Useful if you're picking a primary CLI rather than committing to Claude Code, or if you need to argue with a teammate who's already committed to a different one. [Best MCP Servers for Devs (Firecrawl)](https://www.firecrawl.dev/blog/best-mcp-servers-for-developers). The 2026 edition. Short, opinionated, sensible picks. Some overlap with this post; their angle is more "what does Firecrawl recommend pairing with Firecrawl." Worth a read for a second opinion on the same canon. [Claude Code MCP Servers: How to Connect, Configure, and Use Them (Builder.io)](https://www.builder.io/blog/claude-code-mcp-servers). The actual config walkthrough I'd hand a teammate who's never wired up an MCP. Covers `.mcp.json`, scope, transports, and OAuth flows. March 2026, still accurate. [Agentmaxxing (Vibecoding)](https://vibecoding.app/blog/agentmaxxing). Provocative essay on running many agents in parallel. Read it for the framing, not the prescriptions. The author leans further into autonomy than I do, but the piece is the cleanest articulation I've seen of why the next bottleneck for solo devs is coordination, not capability. ## Skills and extensions Skills are the muscle memory of Claude Code. A good skill is the difference between Claude flailing and Claude executing a workflow you'd recognize as "how a senior would do it." **Superpowers (obra).** [github.com/obra/superpowers](https://github.com/obra/superpowers) by Jesse Vincent. v5.1.0, very large following. Brainstorm, plan, and execute slash commands plus twenty-plus battle-tested skills covering TDD, debugging, code review, and more. It works across Claude Code, Codex, Gemini CLI, and OpenCode. If you install nothing else from this section, install this. **Taste Skill.** [tasteskill.dev](https://www.tasteskill.dev) (source: [github.com/Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill)). An anti-slop frontend skill. The pitch: Claude generates UI that doesn't immediately read as AI-generated. Mileage varies by stack, but the underlying instructions are worth reading even if you don't install the skill verbatim. **Chrome CDP.** [github.com/pasky/chrome-cdp-skill](https://github.com/pasky/chrome-cdp-skill) wires Claude into a Chrome DevTools Protocol session. Lighter-weight than Playwright when you just need to inspect a running page. Alternatives exist (pengelbrecht's variant, the obra/superpowers-chrome bundle), but pasky's is the one I've reached for. **Delivery Lifecycle.** [github.com/levnikolaevich/claude-code-skills](https://github.com/levnikolaevich/claude-code-skills) is a full Agile-pipeline plugin suite. Backlog grooming, sprint planning, release notes, the whole choreography. If your team genuinely runs Agile, this maps to that ritual cleanly. **Codex Settings.** [github.com/fcakyon/claude-codex-settings](https://github.com/fcakyon/claude-codex-settings) is a personal Claude Code plus Codex setup pack. Worth a read even if you don't adopt it: it shows what a thoughtful settings.json plus skills layout looks like. **CLUI.** [github.com/lcoutodemos/clui-cc](https://github.com/lcoutodemos/clui-cc) is a macOS-only TUI overlay for Claude Code. Niche but lovely if you live in the terminal and want a slightly nicer surface than the raw CLI. **Clawd Cursor.** [clawdcursor.com](https://clawdcursor.com), v0.8.8. Desktop-control via MCP. It lets Claude move your mouse and read your screen. Powerful and slightly unsettling. Worth experimenting with on a sandbox machine before pointing it at your real one. (Note: don't confuse this with `rullerzhou-afk/clawd-on-desk`, which is an unrelated pixel-art desktop pet.) > **Sublime pick:** Superpowers. Install it today, thank me later. The brainstorm/plan/execute trio alone is worth the install. Once you have the muscle memory of `/brainstorm` before any non-trivial change, you won't go back. A note on installation hygiene for skills: read the `SKILL.md` before you install. A skill is essentially "extra system prompt the agent will follow." Treat it with the same scrutiny you'd give a dependency in production code. If the description is vague or the maintainer doesn't explain the intent, that's a signal to skip. ## Terminal multiplexers When one Claude session isn't enough. The whole category exists because git worktrees plus tmux is awkward, and several teams independently decided to fix it. **cmux.** [cmux.com](https://cmux.com) is a native macOS app, Swift/AppKit, launched February 2026. It manages parallel Claude sessions with a real GUI. If you're on a Mac and don't want to live in tmux, this is the most polished option I've used. **claude-squad.** [github.com/smtg-ai/claude-squad](https://github.com/smtg-ai/claude-squad), v1.0.17. Tmux plus git worktrees, but the rough edges are sanded down. Spin up N agents on N branches, watch them run, merge what you like. I've been running it for several months and it's the tool I open first when I want to fan out work. **dmux.** [github.com/standardagents/dmux](https://github.com/standardagents/dmux) (also at dmux.ai). Cross-agent: it manages sessions for Claude Code, Codex, OpenCode, Cline, Gemini CLI, and more. Pick this if you intentionally use multiple agent CLIs and want one harness over all of them. **mux (coder/mux).** [github.com/coder/mux](https://github.com/coder/mux), v0.24.0. From the Coder team, so it leans toward remote dev environments. Active, well-maintained, but unless you're already a Coder shop, the others are easier starts. ### Multiplexer comparison cmux claude-squad tmux (raw) **Setup time** 60 seconds (download `.dmg`, open) 5 minutes (`brew install`, plus tmux + git worktrees) 0 (already installed) **Platform** macOS only macOS, Linux macOS, Linux, BSD, anywhere **Best for** Mac devs who want a GUI for parallel agents Terminal-native devs running 3-10 parallel sessions on branches Engineers who already have a tmux config they love **Skip if** You're on Linux or you actually like the terminal You don't use git worktrees or you hate tmux You don't already know tmux; the learning curve isn't worth it just for this > **Sublime pick:** claude-squad if you live in the terminal, cmux if you're on a Mac and want a real window manager. Don't bother with raw tmux unless you already love tmux. A workflow tip that pays off across all three: pair the multiplexer with git worktrees, not branches. Each agent gets its own checkout, no `git stash` dance, no fighting over uncommitted changes. claude-squad does this automatically. cmux makes it a one-click affair. Raw tmux requires you to wire it up yourself, which is most of why the dedicated tools exist. ## Agent frameworks When you're past one agent and need real coordination. Several of these projects use the word "swarm." Some of them mean it; most don't. **ClawTeam.** [github.com/HKUDS/ClawTeam](https://github.com/HKUDS/ClawTeam) is HKUDS's multi-agent swarm coordinator. Academic provenance, which means the code is more interesting to read than to deploy, but the patterns are educational. **CLI-Anything.** [github.com/HKUDS/CLI-Anything](https://github.com/HKUDS/CLI-Anything), also HKUDS. The pitch: generate a CLI for any app. As an experiment in agent-as-codegen it's fun. As a daily driver, treat it as alpha. **Autoresearch.** [github.com/uditgoenka/autoresearch](https://github.com/uditgoenka/autoresearch) implements Karpathy's autonomous-loop pattern as a skill. If you want Claude to grind on an open-ended research task without a human in the loop, this is the cleanest minimal version I've seen. **Hermes Agent.** [github.com/NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) by Nous Research. Integrates with Claude Code via a `/cc` command. If you're already in the Nous ecosystem (or want to be), this is the bridge. **CopilotKit.** [github.com/CopilotKit/CopilotKit](https://github.com/CopilotKit/CopilotKit), v1.57.0, 30.9k stars. Less of a Claude-Code-specific tool and more an agent UI framework. Useful if you're shipping agent features inside a product, not just driving Claude from your terminal. **Ruflo.** [github.com/ruvnet/ruflo](https://github.com/ruvnet/ruflo) (formerly Claude Flow, rebranded around January 2026). This is the multi-agent orchestration layer I actually reach for. Persistent memory, swarm coordination, and a deep MCP surface that exposes everything from observability to security audits. I dispatched a Ruflo swarm earlier today through MCP and the agents coordinated cleanly without the prompt-engineering pep talk I'd usually need. It deserves more than a paragraph, which is why we wrote [the Ruflo deep dive](https://sublimecoding.com/blog/ruflo-claude-flow-multi-agent-deep-dive) as a companion to this post. > **Sublime pick:** Ruflo. It's the only framework in this list I'd commit to before shipping a project. Read [our Ruflo breakdown](https://sublimecoding.com/blog/ruflo-claude-flow-multi-agent-deep-dive) for the install, the tradeoffs, and the failure modes. ## Automation and infra Shipping agents to production. The tools here are less glamorous than the framework section but more important if you're running anything past a hobby project. **n8n-as-code.** [github.com/EtienneLescot/n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) brings n8n's 537 nodes and seven thousand templates under Claude Code as code. If you've been writing automation glue by hand and feel the urge to graduate, this is the bridge. **Cloak Browser.** [github.com/CloakHQ/CloakBrowser](https://github.com/CloakHQ/CloakBrowser) is a stealth Chromium project (not Claude-Code-specific). It integrates with Claude Code via the separate `overtimepog/CloakMCP` server. Worth knowing about if your agent work runs into bot-detection walls; not worth installing speculatively. **OpenLogs.** [github.com/charlietlamb/openlogs](https://github.com/charlietlamb/openlogs). I'm including this with a caveat: it's a small, single-contributor project. The shape is right (open observability for AI agents), but evaluate for yourself before betting infrastructure on it. Treat it as a starting point, not a finished platform. **Vercel Chat SDK.** [github.com/vercel/ai-chatbot](https://github.com/vercel/ai-chatbot), 20.3k stars. The Next.js AI Chatbot template, commonly called the "Chat SDK." Best path I know for shipping a chat-driven AI feature in a real product without writing the harness yourself. **Self-hosted infra (Happy Engineering).** [happy.engineering](https://happy.engineering) plus [github.com/slopus/happy](https://github.com/slopus/happy). Mobile and web client for Claude Code with a self-hostable relay. If you want to drive Claude Code from your phone without sending traffic through someone else's server, this is the path. > **Sublime pick:** Vercel Chat SDK if you're shipping AI features fast. For observability, OpenLogs is promising but I'd run it side-by-side with whatever you already use until you've stress-tested it. The under-discussed move in this section is Happy Engineering's self-hosted relay. Most agent infrastructure assumes you're driving from a laptop and never need to step away. If you want to dispatch work from your phone (kick off a long task at the gym, check on a stuck agent at dinner), the relay model is the only realistic way to do that without piping all your traffic through someone else's hosted control plane. Whether you self-host Happy or roll your own, it's worth thinking about the architecture before you need it. ## Sublime's recommended starter stack If you only install five things, install these. They cover the highest-leverage ground for a working developer who's already past "hello world" with Claude Code. **1. Superpowers (skill bundle).** Brainstorm, plan, execute, plus the canonical TDD and debugging skills. `claude plugin install obra/superpowers ``` **2. claude-squad (parallel sessions).** Fan out across worktrees without rebuilding tmux from scratch. brew install smtg-ai/tap/claude-squad ``` **3. Context7 (live docs MCP).** Stop watching Claude write code against last year's API. claude mcp add context7 -- npx -y @upstash/context7-mcp ``` **4. Sentry MCP (production debugging).** Wire your error feed into the model that's writing the fix. claude mcp add --transport http sentry https://mcp.sentry.dev/mcp ``` **5. Ruflo (when you're ready to scale).** When one agent isn't enough and you want orchestration with persistent memory rather than a manual claude-squad` round-robin. The full install and configuration walkthrough is in [the Ruflo deep dive](https://sublimecoding.com/blog/ruflo-claude-flow-multi-agent-deep-dive). `npm install -g ruflo ``` These five cover the long tail. Skills give you workflow muscle memory, claude-squad gives you parallelism, Context7 stops the hallucination of stale APIs, Sentry MCP gives you a feedback loop with production, and Ruflo is what you reach for when the workflow itself needs to become an agent. ## What we left off and why Honest cuts, in case you saw a tool elsewhere and wondered why it's not here: The original outline included a generic "PostgreSQL MCP" entry. The original reference server is archived. I swapped in crystaldba/postgres-mcp` instead because at least one Postgres entry deserves to be in the bible, and that's the maintained one I'd actually point a friend at. A handful of "awesome" lists got cut because they share a name with four or five other repos. If a list isn't unambiguously the canonical thing, including it just creates more discovery work for you. A few agent frameworks were dropped for not having a verifiable canonical URL. If I can't link to the project root with confidence, I can't recommend installing it. "Loop" appeared in the original outline as a third-party skill. The `/loop` command is now bundled in Claude Code itself, documented at [code.claude.com/docs/en/scheduled-tasks](https://code.claude.com/docs/en/scheduled-tasks). You already have it. A bundle of `nicholasoxford/*` MCP servers were referenced in source material I was working from. They don't exist. The actual Anthropic-maintained reference servers are at [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers). When you see an MCP install command from a screenshot, check the org before you run it. ## What's coming next This is Post 1. Post 2 is the long-form companion: [the Ruflo deep dive](https://sublimecoding.com/blog/ruflo-claude-flow-multi-agent-deep-dive), where we go past the recommendation and into the install, the architecture, the orchestration model, and the specific failure modes I've hit running it on real work. Sublime Coding is freshly pivoted toward practical, opinionated coverage of AI coding tools and developer workflows. If that's the kind of thing you want in your inbox without filler, the newsletter signup is below. We don't run sponsored picks. If a tool I've missed deserves a spot in the next revision of this list, drop it in the comments with a one-sentence pitch for why. Especially interested in things you've actually run for more than a week. ## Read this next - [**How I Prompt Claude as a Staff Engineer (50 Prompts I Actually Use)**](https://sublimecoding.com/blog/staff-engineer-claude-prompts) — The companion piece: the prompts that put these 46 tools to work. - [**Prompt Skills, Not Claude: Four Rules from Anthropic's Engineers**](https://sublimecoding.com/blog/prompt-skills-not-claude) — The skill-authoring rules behind the best entries in this list. --- ## The Claude Code Plugin Stack I Actually Run URL: https://sublimecoding.com/blog/claude-code-plugin-stack Published: 2026-07-06 Tags: AI tools, productivity, engineering > **TL;DR:** Plugins bundle skills, hooks, and MCP configs into one install, and it’s tempting to grab whatever looks useful. I don’t. A plugin earns a slot in my stack only if it encodes a discipline I’d otherwise have to reconstruct from memory every session, or a piece of domain expertise I don’t personally hold. Almost all of that value lives in one category — process and workflow — and it’s the category most people skip in favor of flashier domain-specific tooling. The domain stuff is real too, but only on the projects that actually need it; I don’t run Elixir-specific plugins against a Python repo just because they’re installed globally. Below is the actual stack, organized by why each one is there. ## The question that decides if a plugin earns a slot I run a lot of different codebases in a given month — my own projects, client engagements where I’m the fractional CTO or vCISO walking into someone else’s repo, greenfield prototypes. Every one of those sessions starts the same way: an agent with no memory of yesterday, reading whatever context I hand it. Plugins are how I hand it more than a prompt. But “more context” isn’t automatically good. A plugin that just wraps a prompt template I could type myself isn’t worth a permanent install — it’s clutter that shows up in every session whether the project needs it or not. The bar I actually use: **does this encode a repeatable discipline or real domain expertise I’d otherwise have to reconstruct from scratch, every single time?** If yes, it’s earned a slot. If it’s just “a thing that exists in the marketplace,” it’s not. That filter sorts my stack into three very unequal piles. The first is small in plugin count and enormous in leverage. The other two are real, but scoped tight. ## Process plugins: where almost all the value lives These don’t teach the agent anything about a tech stack. They change *how* the agent and I work together, which means they pay off on every project, every day, regardless of language or domain. If I could only keep one category, it’s this one. **Superpowers’ brainstorming-first workflow.** The single highest-leverage habit I’ve adopted is refusing to let an agent write code before it’s asked me clarifying questions. Left alone, a coding agent will take an ambiguous ask and confidently build *something* — fast, plausible, and frequently not what I meant. A brainstorm-first discipline forces a short back-and-forth before the first line of code: what’s actually broken, who hits this, what does “done” look like. It costs two minutes. Skipping it costs an afternoon of reviewing code against the wrong spec. I’ve written before about [prompting skills instead of prompting the model one sentence at a time](https://sublimecoding.com/blog/prompt-skills-not-claude) — this is the clearest example of a skill that’s worth more than any prompt, because the discipline only works if it fires *every* time, unprompted, and a plugin is how you make “every time” actually happen instead of “when I remember to ask for it.” **Superpowers’ systematic-debugging discipline.** The default failure mode for an agent staring at a bug is the same as the default failure mode for a junior engineer: jump straight to the first plausible fix, apply it, declare victory. That’s a symptom patch, not a root cause fix, and it’s how the same bug comes back three commits later wearing a different hat. A debugging plugin worth keeping forces the boring sequence — reproduce, isolate, form a hypothesis, test the hypothesis, *then* fix — before any edit gets made. I’ve catalogued the specific bug patterns this catches in [the AI coding agent bugs I catch every week](https://sublimecoding.com/blog/things-ai-coding-agents-get-wrong); most of them are exactly this shortcut, taken with confidence. **The `code-review` skill, run at adjustable effort.** Not every diff deserves the same scrutiny. A one-line config change and a new auth boundary should not get the same review budget, and an agent that reviews everything at maximum depth is as useless as one that reviews everything at a skim — the first wastes your afternoon, the second misses the thing that matters. The review plugins I keep let me dial the pass: quick sanity check on a small diff, full adversarial read on anything touching money, auth, or a data boundary. That’s the same judgment I apply running an actual PR review, encoded so it doesn’t depend on me remembering to ask for it that day. **Ponytail’s lazy-engineer discipline.** This is the one that saves the most wall-clock time and gets skipped the most, because it doesn’t feel like it’s “doing” anything — it’s a plugin whose entire job is to argue *against* writing code. Reuse what’s already in the codebase before adding a new file. Reach for the standard library before a dependency. Treat YAGNI as a reflex, not a suggestion, and push back out loud when a task is drifting toward an interface with one implementation or a config knob for a value that never changes. Left alone, agents over-build — they’ll happily scaffold an abstraction layer for a feature that will only ever have one caller. The instinct to ship the smallest working slice and question the rest in the same breath is the same one I’ve written about for [shipping complex features as vertical slices](https://sublimecoding.com/blog/smallest-possible-slice-shipping-complex-features); a lazy-engineer plugin is that instinct running inside the agent instead of just inside my own head. **Caveman’s terse-communication style.** Smaller win, but a real one: a plugin that strips the filler out of an agent’s responses — no recap of code it just showed you, no essay defending a simplification that’s already obvious, no “great question!” padding. It doesn’t change what gets built, but it changes how fast I can read the output and decide what to do next, and over a full day of agent sessions that adds up to real time back. **The `claude-md-management` plugin.** A project’s CLAUDE.md is executable tribal knowledge — the operational realities, the gotchas, the “don’t do X, here’s why” that a new session needs and a stale file quietly stops providing. Left unmaintained, it rots the same way any onboarding doc rots: it drifts from what’s actually true, and a new session inherits confidently wrong context. A plugin that periodically checks the file against the current codebase and flags what’s stale is cheap insurance against that drift. I’ve made the underlying case for treating the file this way in [your CLAUDE.md is the onboarding doc you never wrote](https://sublimecoding.com/blog/claude-md-onboarding-doc). Every plugin costs something even when it’s not doing anything — context the agent reads, a config surface that can drift, one more thing to explain to the next person. ## Domain plugins: deep, but scoped to the project that needs them The second pile is domain-specific — plugins that encode years of framework or vertical idiom I don’t want to reconstruct from a blog post every time I touch that stack. These are real, and they’re deep, but the discipline here is different: install them *per project*, not globally. Dragging Elixir-specific tooling into a Python repo doesn’t help — at best it’s inert weight, at worst it’s an agent trying to apply the wrong idiom to the wrong runtime. **The `elixir-phoenix` plugin.** On the client engagements and personal projects running on the BEAM, this is the deepest single install I keep. Ash resource patterns, LiveView idioms (streams versus assigns, async mounts, the PubSub-subscribe-after-`connected?/1` rule that trips up every agent that hasn’t seen it), Oban worker conventions, and an N+1 detector that actually understands the difference between a `has_many` that needs a separate query and a `belongs_to` that needs a join. I’ve written about a chunk of this idiom directly — [twelve LiveView patterns that hold up in production](https://sublimecoding.com/blog/phoenix-liveview-patterns-production) and [running an agent loop on Oban as a durable runtime](https://sublimecoding.com/blog/oban-durable-ai-agent-runtime) are both things a generic coding agent will get subtly wrong without this kind of scaffolding, because the failure modes are BEAM-specific and nowhere near as intuitive as they look from a Rails or Node background. **The `claude-seo` plugin.** Technical SEO, schema markup correctness, content-quality checks, and — new enough that most people haven’t built a habit around it yet — readability for AI-search and generative engine results, not just the classic ten blue links. I keep this scoped to content and marketing-site work specifically; it has nothing useful to say about a backend service repo, and installing it everywhere just adds noise to sessions where it doesn’t apply. **The `marketing-skills` plugin.** Copywriting, positioning, launch-plan structure. I reach for this maybe one week in eight, when the task in front of me is genuinely go-to-market rather than code — and that’s exactly why it stays out of the default stack. It’s real expertise I don’t want to reconstruct from scratch when I need it, but it has no business being active during an engineering session where it’ll happily suggest a rewrite of a docstring in marketing voice. The pattern across all three: depth is the point, and scope is the discipline. A domain plugin that’s genuinely good encodes something you’d otherwise have to relearn or reverse-engineer — that’s worth the install. It’s also worth *uninstalling*, or at minimum not enabling, the moment you’re not on that kind of project. The failure mode isn’t picking a bad domain plugin; it’s letting a good one bleed context into work it wasn’t built for. ## Utility plugins: small, single-purpose, no ceremony The third pile is the smallest per-item but still earns its keep — narrow tools that solve one recurring annoyance and then get out of the way. **A design-taste plugin like `design-taste-frontend`.** Left to its own aesthetic judgment, a coding agent defaults to the same handful of generic patterns — the same gradient, the same card-with-shadow, the same spacing scale every AI-assisted landing page seems to land on. A design-taste plugin that actively blocks those defaults and pushes toward something with an actual point of view is worth keeping specifically because the failure mode it prevents is invisible until a real person looks at the page and it feels like every other AI-built site. **The `dataviz` skill.** Similar idea, narrower scope: default chart output from an agent is functional and forgettable. A skill that encodes actual data-viz taste — when a bar chart beats a line chart, when to just use a table instead of forcing a visualization the data doesn’t support — saves a full review-and-redo cycle on anything involving a dashboard or report. **The `commit-commands` plugin.** Not glamorous, but they save real minutes: consistent commit message formatting, a PR description scaffold that pulls the actual diff summary instead of a placeholder, review-comment triage. This is the kind of thing that’s genuinely one line of value per use, which is exactly why it should be a saved command rather than something I re-explain every time. **The `update-config` skill.** Slightly meta — a plugin for configuring the harness itself. Useful specifically because hook and permission configuration is exactly the kind of thing you set up once, forget the syntax for, and need again three months later on a different project. **The `keybindings-help` skill.** Small, personal, and entirely about shaving friction off the parts of the workflow that happen hundreds of times a day. Not a discipline, not domain expertise — just ergonomics, and I don’t pretend it’s more than that. **The `obsidian` plugin.** An Obsidian-style link into a personal knowledge base, so an agent can pull in and write back durable notes instead of every insight evaporating at the end of the session. This is the same problem CLAUDE.md solves at the project level, one layer up — a persistent memory an agent actually reads instead of a wiki that only humans open. I went deep on the architecture for doing this well in [building an LLM wiki your coding agent actually reads](https://sublimecoding.com/blog/build-an-llm-wiki-for-your-agent). ## What doesn’t make the cut If you want the full field before you start cutting, I catalogued [46 tools worth knowing](https://sublimecoding.com/blog/claude-code-resource-bible) elsewhere — that post is the breadth survey; this one is the subset that actually survived the filter above and stayed installed. Notice what’s not up there: nothing installed because it trended, nothing that duplicates a discipline a different plugin already covers, nothing domain-specific running outside the project it was built for. The failure mode I see most often — in my own stack early on, and in engineers I’ve worked with since — isn’t under-installing. It’s the opposite: a plugin folder that’s grown by accretion, full of things that seemed useful in the moment and now just add noise and context bloat to every session, including the ones where they don’t apply. Even an unused plugin isn’t free — it’s tokens spent on context nobody asked for, one more surface that can quietly drift out of date, one more thing to explain when a new teammate asks why it’s there. The process category is where I’d tell anyone starting from zero to spend their first real effort — a brainstorm-first habit and a real debugging discipline change the shape of every session you’ll ever run, on any stack. The domain plugins are worth going deep on, but only exactly as deep as the projects you’re actually running, and only on those projects. Everything else is quality-of-life, worth having, not worth confusing for either of the two things above it. If you’re standing up this kind of workflow across a team rather than just for yourself — deciding which disciplines get codified, which stay person-specific, and where the line is between a real productivity investment and tool sprawl — that’s a conversation I have often in [fractional engineering leadership engagements](https://sublimecoding.com/consulting). --- ## AIOS: An AI Operating System in Plain Markdown URL: https://sublimecoding.com/blog/aios-ai-operating-system-markdown Published: 2026-07-09 Tags: AI tools, agents, productivity > **TL;DR:** I open-sourced the vault I actually run — [AIOS](https://github.com/sublimecoder/aios), MIT-licensed, plain markdown and shell scripts, no database, no lock-in. Clone it, run `/aios-bootstrap`, answer questions for ten minutes, and you have a vault your coding agent reads at the start of every session and writes back to at the end. Wire a repo to it with one command and the loop closes: code → digest → nightly ingest → brain → next session starts smarter than the last one. It works with Claude Code today because that’s what I use; the canonical behavior is markdown, so swapping the tool later doesn’t cost you the vault. ## Why I open-sourced it I’ve written the theory here before. [Building an AI memory that outlives any single agent](https://sublimecoding.com/blog/ai-memory-outlives-any-agent) made the portability argument: your accumulated context is worthless the day you switch tools if it only lives in one vendor’s chat history or config format. [Build an LLM wiki your coding agent actually reads](https://sublimecoding.com/blog/build-an-llm-wiki-for-your-agent) laid out the architecture — three layers (immutable sources, model-written synthesis, a read-first index), three operations (ingest, query, lint), two rules that stop it rotting. Both posts describe a shape. Neither one hands you the thing. AIOS is the thing. It’s the actual vault I run across every repo I touch, templated so you can clone it and have your own running in about fifteen minutes. The v2 rewrite went further and [made scope the first path segment](https://sublimecoding.com/blog/aios-v2-scope-is-the-filesystem), so the write guard became prefix validation instead of four coordinated config edits. The design bet underneath it is an inversion of how most people think about agent memory. The usual fixes — a bigger context window, a vector store, a `CLAUDE.md` that grows until nobody reads it — all treat memory as a *retrieval* problem: throw everything in, search it at query time. AIOS treats it as an **editorial** problem instead. Sessions produce raw signal — commits, diffs, decisions. A periodic ingest pass compounds that signal into durable, hand-shaped project notes. A read-first index — the Knowledge Map — points at them, one line each, cheap enough to load whole. Why that editorial pass beats a bigger context window or a vector store is the whole case the wiki post makes — AIOS is that architecture shipped, not re-argued here. ## Setup: clone it, re-init git, and let it interview you The whole quickstart: `[](#cb1-1)git clone https://github.com/sublimecoder/aios ~/code/aios [](#cb1-2)cd ~/code/aios [](#cb1-3)rm -rf .git && git init # fresh history — see below [](#cb1-4)claude ``` The rm -rf .git && git init` step isn’t cosmetic. You’re about to fill this repo with identity files, project history, and decisions you made under deadline pressure — the exact things you don’t want sitting in a public template’s commit graph, discoverable by anyone who forks upstream before you scrub it. Re-initializing gives you a clean history from commit one, and git history is forever in the direction that matters: a later fix can’t un-share something an earlier commit exposed. If any part of what you put in this vault is private, the repo needs to be private too — that’s a decision to make before the first commit, not after. Once you’re in a Claude Code session inside the fresh clone, run: `/aios-bootstrap ``` That command just reads BOOTSTRAP.md` and executes it as a prompt — the whole bootstrap process is itself a markdown file, which is the pattern repeating at the meta level. It interviews you for about ten minutes, mostly you answering questions, and writes the vault around your actual answers instead of plausible-sounding defaults. A few things it’s strict about: - **Interview first, write second.** It won’t create a file until it has real answers, asked in small batches instead of one long form. - **Never invents biography.** If it doesn’t know something about you, it asks. An identity file full of plausible-but-wrong facts is worse than an empty one. - **Default to less.** One scope, no wall, no optional subsystems, unless your answers actually demand more. The first real question is about **scopes** — a scope is a bucket of context: a job, a side project, a client. Its whole job is routing, keeping one project’s accumulated memory out of an unrelated one and telling the agent which identity file governs the current session. And here’s the honest part most tools with this feature won’t tell you: **one scope is the right answer for most people.** The bootstrap won’t talk you into more. If you hesitate when it asks how many contexts you need, the correct answer is one — adding a second scope later is a two-line edit to a TSV file, not a redesign. ## Wiring repos: the loop that makes it compound A vault that only holds what you type into it by hand is just notes. The part that makes AIOS actually compound is wiring your code repos into it, so sessions feed the vault automatically: `[](#cb3-1)scripts/aios-wire-repo.sh ~/code/acme-api work [](#cb3-2)scripts/aios-install-nightly.sh # drain queue at 03:00 ``` The loop it closes looks like this: code session in any repo │ │ SessionStart → aios-context.sh │ loads the project's brain into the session ▼ you work │ │ Stop → aios-digest.sh │ queues a signal-only digest in the vault │ (branch, commits, diff-stat) ▼ /aios-ingest (nightly, or by hand) │ │ synthesizes the queue into durable facts ▼ AIOS/Projects/