<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonathan Tavares</title>
    <description>The latest articles on DEV Community by Jonathan Tavares (@jtavares).</description>
    <link>https://dev.to/jtavares</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3801475%2Fd9666561-f4af-41d1-b5c9-96383901421f.jpg</url>
      <title>DEV Community: Jonathan Tavares</title>
      <link>https://dev.to/jtavares</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jtavares"/>
    <language>en</language>
    <item>
      <title>How to write the code review SKILL.md your team actually needs</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Tue, 04 Aug 2026 20:15:22 +0000</pubDate>
      <link>https://dev.to/jtavares/how-to-write-the-code-review-skillmd-your-team-actually-needs-3gab</link>
      <guid>https://dev.to/jtavares/how-to-write-the-code-review-skillmd-your-team-actually-needs-3gab</guid>
      <description>&lt;p&gt;Prompts such as "review this code thoroughly" hide several choices because nobody has defined what "thorough" means. It might mean runtime behavior, architecture, security, product intent, or all of them at once. Ask for everything, and the review tends to become a tour of the diff: one naming comment, half an investigation into a race, a generic request for tests, and no clear opinion on whether the change should land.&lt;/p&gt;

&lt;p&gt;We ran into that problem while building &lt;a href="https://github.com/we-are-singular/skills/tree/main/skills/singular-code-review" rel="noopener noreferrer"&gt;our own review skill&lt;/a&gt;. Writing the prompt was easier than deciding what we wanted the reviewer to notice, which evidence it should trust, when it should stay quiet, and what should happen after it finds something.&lt;/p&gt;

&lt;p&gt;Before writing much of our own, we read other teams' review skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned from other review skills
&lt;/h2&gt;

&lt;p&gt;Our research corpus covered 33 public &lt;code&gt;SKILL.md&lt;/code&gt; files and related review workflows, from company-maintained approaches by Cursor, Sentry, &lt;a href="https://github.com/anthropics/claude-code/blob/7ef6eec9d9ba84ea6f233f26c45f1df5c5991843/plugins/code-review/commands/code-review.md" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, and .NET MAUI to &lt;a href="https://github.com/xpepper/pr-review-agent-skill" rel="noopener noreferrer"&gt;independent projects&lt;/a&gt; with a single GitHub star at the time of our research. That range was deliberate: we wanted a broad cross-section of how the ecosystem defines review, not only the answers from its most visible teams.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cursor/plugins/blob/fa16d695b35ccf4ea179d976e5aaee0834a25b0b/thermos/skills/thermo-nuclear-code-quality-review/SKILL.md" rel="noopener noreferrer"&gt;Cursor's Thermo-Nuclear Code Quality Review&lt;/a&gt; was a major starting point for us. Cursor separates strict structural review from a second &lt;a href="https://github.com/cursor/plugins/blob/fa16d695b35ccf4ea179d976e5aaee0834a25b0b/thermos/skills/thermo-nuclear-review/SKILL.md" rel="noopener noreferrer"&gt;security-and-correctness review&lt;/a&gt;. That separation was useful to us: a reviewer hunting concrete bugs should behave differently from one challenging a tangled design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills/blob/2ab958093e83e0ec752e6c1c5932da465bf23e0c/skills/engineering/code-review/SKILL.md" rel="noopener noreferrer"&gt;Matt Pocock's review skill&lt;/a&gt; and &lt;a href="https://github.com/getsentry/skills/blob/e7a87fa72645158f9b5e722cbb1c7e09266f48f1/skills/code-review/SKILL.md" rel="noopener noreferrer"&gt;Sentry's compact policy&lt;/a&gt; pushed us toward an explicit policy: repository standards and specification fit are separate questions and, most importantly, &lt;strong&gt;stylistic preference should not block a pull request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/trailofbits/skills/blob/9ea55c598763f7cb87ab56933d773d7dc34344a0/plugins/differential-review/skills/differential-review/SKILL.md" rel="noopener noreferrer"&gt;Trail of Bits&lt;/a&gt; and &lt;a href="https://github.com/dotnet/maui/blob/a1521eda4980d8b6a890323ee3273e2e96f71703/.github/skills/code-review/SKILL.md" rel="noopener noreferrer"&gt;.NET MAUI's project-owned reviewer&lt;/a&gt; showed another useful distinction. A specialist can go deep on security, while a project-owned reviewer can read full files, callers, history, previous reviews, and CI.&lt;/p&gt;

&lt;p&gt;The research left us with a design problem we could name and by the time we wrote our skill, we knew what decision the review should support and where it should look for intent. We had also settled which "lanes" to use and when each "lane" runs, what evidence earns a comment, how severity affects the author, and whether the skill belongs locally, in CI, or both. The model then had a concrete brief to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution we chose: one policy, several lanes, one synthesis
&lt;/h2&gt;

&lt;p&gt;We decided that our reviewer should act like a senior engineer deciding whether a change is safe and coherent enough to land. The reviewer asks a team of specialized sub-reviewers to go deep on different aspects of the code, then synthesizes and deduplicates their feedback. We are not merely bug hunting: code can work in isolation and still fail to solve the ticket or fulfill the client request, break a caller's assumption, duplicate an existing abstraction, or cut across patterns that keep the repository coherent.&lt;/p&gt;

&lt;p&gt;A lane is a focused sub-reviewer task (or sub-agent if the environment supports delegation). Each lane receives the same context, but it investigates a different question. The lanes can run in parallel when the environment supports delegation or sequentially otherwise. They keep separate notes and history, so an architecture opinion cannot drown out a concrete runtime bug, and a bug hunt does not quietly invent product requirements or nice-to-have features.&lt;/p&gt;

&lt;p&gt;We work primarily in TypeScript and JavaScript across a set of opinionated repositories. Our monorepos share patterns and vocabulary, including consistent meanings for "backend," "frontend," and "package." Each repository also carries an &lt;code&gt;AGENTS.md&lt;/code&gt; file with its project-specific rules. That gives us a consistent baseline without pretending every codebase is identical.&lt;/p&gt;

&lt;p&gt;The skill is portable, but it carries assumptions and biases from our own codebases. That is why we are explaining the decisions behind it rather than publishing a skill and hoping it works for everyone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before we even start: &lt;code&gt;npm run lint&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A strict linter is the first review step in our policy&lt;/strong&gt;, which lets the model leave a lot out of the review path. Singular repositories use deliberately strict linting built from community rulesets and a smaller set of our own rules. &lt;strong&gt;Formatting, import order, naming conventions, and other stylistic decisions are settled before a reviewer or model can turn them into review comments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a recurring bad pattern can be recognized programmatically, we would rather make it a rule. One example is the spray of tiny one-line wrapper helpers that newer models like to add. If a project has decided against that shape, &lt;strong&gt;one precise lint failure is cheaper than having the same discussion on every pull request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Strict linting also gives a coding agent a programmatic self-check: run the tool, receive a file name, line number, and rule, then fix the failure before asking for judgment. The model begins where lint stops: intent, blast radius, runtime behavior, architecture, and risks that need repository context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the context before asking for judgment
&lt;/h3&gt;

&lt;p&gt;Before a lane even sees the code changes, we run a small deterministic discovery pass. Our local &lt;a href="https://github.com/we-are-singular/skills/blob/fc5945da0cbc4fb11007ca97118d0bf4d3e51867/skills/singular-code-review/scripts/collect-review-context.sh" rel="noopener noreferrer"&gt;context collector&lt;/a&gt; establishes the review scope, records a base when one is supplied or inferred, lists changed and untracked files, summarizes the diff, finds applicable repository documents, and can include the full patch.&lt;/p&gt;

&lt;p&gt;In CI, a pull-request runner builds normalized context from the PR description, commits, timeline, review discussions, and diff. The review policy then instructs the reviewer to inspect any linked ticket or plan as an additional source of intent. Our version of context engineering is unfashionably mechanical: &lt;strong&gt;give the model a reliable starting point instead of hoping it remembers how to look.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The deterministic pass establishes a discovery floor. It cannot calculate a semantic blast radius, but it makes sure the model sees the right files, diff, and intent. The lanes then do the semantic work and further investigation. They follow changed symbols into their callers, callees, downstream consumers, state transitions, external contracts, and side effects. They compare the resulting behavior with the stated intent, nearest repository rules, and existing parallel implementations. The diff shows where the edit is. The surrounding code shows what the edit can change.&lt;/p&gt;

&lt;p&gt;In our runs, &lt;strong&gt;this context pass has noticeably reduced output variance and the number of discovery loops the model needs&lt;/strong&gt;. Reviews have also been faster in practice, although we have not treated that observation as a benchmark.&lt;/p&gt;

&lt;p&gt;Every lane receives that context:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;The question it owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent and contracts&lt;/td&gt;
&lt;td&gt;Does the full effect of the change match what was requested, without quietly changing an external promise?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards and architecture&lt;/td&gt;
&lt;td&gt;Does it fit the repository's documented rules, boundaries, and nearby patterns?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code-path bug hunting&lt;/td&gt;
&lt;td&gt;What changes for callers, callees, downstream consumers, state transitions, and side effects when the code runs?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness, risk, and testing&lt;/td&gt;
&lt;td&gt;Which assumptions fail under bad input, errors, concurrency, security pressure, or missing coverage?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation and commentary&lt;/td&gt;
&lt;td&gt;Did the change leave a public contract or a non-obvious workaround unexplained?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability and simplification&lt;/td&gt;
&lt;td&gt;Did the PR add avoidable branches, abstraction, duplication, or awkward ownership?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That separation only works if every lane uses the same bar for deciding what deserves a comment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furpoeq1nso2bmsbcq70e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furpoeq1nso2bmsbcq70e.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lanes would be noisy without a shared finding bar. A concern has to be introduced or exposed by the change, point to a specific mechanism backed by code or repository evidence, show meaningful impact, and leave the author with a concrete fix or decision. Tool-owned lint failures, personal taste, speculative future work, generic demands for tests, and unrelated old problems should stay out.&lt;/p&gt;

&lt;p&gt;Severity gives each retained finding an operational consequence. The skill defines &lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;question&lt;/code&gt;, and &lt;code&gt;hint&lt;/code&gt;. During synthesis, the instructions map the highest retained severity to a textual verdict in the review body. The runner then submits that body and the validated comments as a GitHub &lt;code&gt;COMMENT&lt;/code&gt; review.&lt;/p&gt;

&lt;p&gt;That is the review policy: what evidence counts, what each lane investigates, what gets suppressed, and what a finding means. It can run without our GitHub infrastructure. The next problem was turning that judgment into a repeatable PR review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability across agents, environments, and platforms
&lt;/h2&gt;

&lt;p&gt;A review policy is more useful when it can meet the change before the pull request does. We want the same skill available in a local coding-agent session, a disposable sandbox, a remote coding environment, and ultimately the specialized Docker image that reviews our pull requests.&lt;/p&gt;

&lt;p&gt;Several coding agents understand the format, including GitHub Copilot, Codex, OpenCode, and Claude Code. &lt;a href="https://docs.github.com/en/copilot/concepts/agents/about-agent-skills" rel="noopener noreferrer"&gt;GitHub Agent Skills&lt;/a&gt; work with Copilot's cloud agent, CLI, code review, and IDE agent mode, for example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;Claude Code skills&lt;/a&gt; use the same &lt;code&gt;SKILL.md&lt;/code&gt; convention, and &lt;a href="https://code.claude.com/docs/en/claude-code-on-the-web#cloud-environments" rel="noopener noreferrer"&gt;Claude's cloud sessions&lt;/a&gt; can load project skills committed to the repository. A sandbox or our own review image can install the same folder without translating the policy into another prompt format.&lt;/p&gt;

&lt;p&gt;The policy stays constant across those environments: deterministic context discovery, review beyond the diff, the same lanes, and the same finding bar. An environment with subagents can run the lanes in parallel; another can run them sequentially.&lt;/p&gt;

&lt;p&gt;Locally, an engineer can review a working tree, staged diff, or branch before pushing. In a remote agent or sandbox, the review happens where the agent wrote the code. When the pull request lands, our Docker runner applies the same policy automatically, then adds PR state, line validation, and controlled publication.&lt;/p&gt;

&lt;p&gt;Authors can meet the review policy before push, while intent, architecture, and blast-radius problems are still cheap to fix. The pull-request review remains an independent pass, but it applies a familiar policy rather than a surprise set of preferences. &lt;strong&gt;The intention is less avoidable back-and-forth, not a ceremonial second check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2v2ge2mzva6q39v53fan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2v2ge2mzva6q39v53fan.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the policy and runner were stable, we were able to compare models without changing the review itself. In practice, current coding models cope very well with this work; they excel at long, horizontal tasks. A review spans the request and several parts of the repository, then asks the model to run separate investigations, reconcile them, and report only the useful findings.&lt;/p&gt;

&lt;p&gt;In our local runs, Claude Opus and SOL have performed well. In daily use, we have also tried MiniMax M3 and, more recently, DeepSeek V4 Flash. We have not noticed a quality drop large enough to justify the premium models for every review. That is an operating impression from our own pull requests, not a controlled benchmark, and another codebase may reach a different answer.&lt;/p&gt;

&lt;p&gt;Holding those variables steady makes the failures more revealing: we can see which model missed context, inflated severity, or produced noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate the reviewer on old disagreements
&lt;/h2&gt;

&lt;p&gt;Old pull requests make better evaluation cases than a polished demo because the team already knows what the reviewer should and should not find. We had years of pull requests with bugs, false positives, and silent cases, plus good and bad comments, replies, and verdicts, all from the same team that would later use the skill. We were fortunate to have a large enough corpus that we could run benchmarks on our own work with our own biases while building and shaping our review skill.&lt;/p&gt;

&lt;p&gt;If you can do the same, start with review disagreements that the team understands: a real bug somebody caught, a false positive that wasted time, a missed requirement, an architecture decision with written context, a retry failure, and a clean change where silence is the correct result. Each case needs expected findings and expected non-findings. Otherwise a reviewer can improve its apparent recall by commenting on everything.&lt;/p&gt;

&lt;p&gt;The skill and runner need separate scorecards. For the skill, look at whether the right lane found the issue, whether the evidence supports it, whether severity matches the consequence, whether multiple lanes produced the same comment, and whether the proposed action is usable. Run the same case more than once so context misses and output variance become visible. Track false positives and useful silence as carefully as caught bugs.&lt;/p&gt;

&lt;p&gt;For the runner, check mechanical failures: invalid anchors, wrong diff sides, duplicate comments, malformed replies, lost context, bad verdict mapping, timeouts, and publication errors. Runtime, token use, and cost belong here too. A brilliant finding attached to the wrong line is still a broken review.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5z8uzew7k3dhcqs4kyu8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5z8uzew7k3dhcqs4kyu8.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also the point to compare models. Use the same case set, policy, context, and runner. Look beyond a single average score: which lane failed, which model inflated severity, which one duplicated findings, and which one spent more time reading without improving the review? We compare price only after looking at those results.&lt;/p&gt;

&lt;p&gt;We used a very large rubric on a set of past pull requests, including public PRs from known libraries and private PRs from our own repositories. We ran the same policy on the same cases across several models, then compared lane coverage, evidence, severity, noise, anchors, runtime, and cost. The results were clear enough to guide our model choice for the next several weeks of dogfooding.&lt;/p&gt;

&lt;p&gt;Keep that harness. As the skill, runner, or model changes, rerun the same cases to catch regressions and confirm improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we landed
&lt;/h2&gt;

&lt;p&gt;After several weeks of using the new skill on dozens of pull requests a day, we are happy with it. Its useful findings are often broader than bugs: a caller whose contract changed, a downstream effect the diff concealed, an implementation that missed the ticket, or a new shape that cut against the rest of the repository. The lanes give each investigation enough room to find evidence without turning the final review into comment confetti. What surprised us most in our own runs was that smaller, cheaper models still caught problems that frontier models at high effort had missed.&lt;/p&gt;

&lt;p&gt;The runner is still a prototype. We are dogfooding it to learn where it fails, how model choice changes the economics, and whether there is a useful SaaS product beyond our own use. We have not committed to a product roadmap. For now, it stays on our own pull requests while we keep collecting failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR: what to do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Put strict linting first and leave tool-owned findings out of the AI review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gather scope, intent, applicable rules, and the exact diff through a deterministic context pass.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Follow changes through callers, callees, downstream effects, and established repository patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Split independent failure modes into focused lanes with a shared finding bar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set the finding bar, suppressions, and severity consequences.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the same policy before push and again when the pull request lands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluate on known bugs, false positives, silent cases, and the same pull requests across models.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add we-are-singular/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; singular-code-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgr1tc8bljp06bfgbkm02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgr1tc8bljp06bfgbkm02.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/we-are-singular/skills/tree/main/skills/singular-code-review" rel="noopener noreferrer"&gt;review skill&lt;/a&gt; is MIT-licensed, and the prototype &lt;a href="https://github.com/we-are-singular/singular-code-review-agent" rel="noopener noreferrer"&gt;GitHub runner&lt;/a&gt; is public for inspection. Start with the decisions above, borrow what fits, and change the rest.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>codereview</category>
      <category>ai</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Unifying Codex, OpenCode, and Pi in Remote Sandboxes with ACP</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:03:11 +0000</pubDate>
      <link>https://dev.to/jtavares/unifying-codex-opencode-and-pi-in-remote-sandboxes-with-acp-3365</link>
      <guid>https://dev.to/jtavares/unifying-codex-opencode-and-pi-in-remote-sandboxes-with-acp-3365</guid>
      <description>&lt;p&gt;Imagine Codex needs to inspect a repository in a remote VM. It needs to read that copy, run commands there, ask for permission, and send updates back to your app. That is a session, not a shell command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://agentclientprotocol.com/get-started/architecture" rel="noopener noreferrer"&gt;Agent Client Protocol&lt;/a&gt;, or ACP, gives a client and a coding agent a shared session contract. The client starts an ACP-capable agent process, &lt;a href="https://agentclientprotocol.com/protocol/v1/initialization" rel="noopener noreferrer"&gt;initialises the connection&lt;/a&gt;, &lt;a href="https://agentclientprotocol.com/protocol/v1/session-setup" rel="noopener noreferrer"&gt;creates a session&lt;/a&gt;, sends prompts, receives updates, handles requests from the agent, and closes the session when it is done. It can use stdin and stdout, which suits an agent running in a remote environment.&lt;/p&gt;

&lt;p&gt;The protocol was built for an app, such as a code editor, to run a coding agent and talk to it. Usually they run on the same machine and exchange messages over stdin and stdout.&lt;/p&gt;

&lt;p&gt;We can use the same arrangement remotely. The app stays where it is; the coding agent runs in a Sandbox. The protocol connects them. &lt;strong&gt;It does not create the Sandbox or install the agent, but Codex, OpenCode, and Pi can all use the same connection once they are running.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shared client uses ACP's &lt;a href="https://agentclientprotocol.com/libraries/typescript" rel="noopener noreferrer"&gt;TypeScript SDK&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Run the agent in the same place as its files and commands.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article uses &lt;a href="https://agent-markup-language.com/" rel="noopener noreferrer"&gt;Agent Markup Language&lt;/a&gt;, aka &lt;code&gt;AML&lt;/code&gt;, as a concrete example. It is an MIT-licensed TypeScript and JSX runtime for composing agent workflows, tools, Sandboxes, and durable Workspaces. We recently moved its built-in coding agents to ACP so their session lifecycle no longer changes with the selected execution environment.&lt;/p&gt;

&lt;p&gt;In AML, Sandbox and Workspace are named runtime resources, not generic labels. A Sandbox supplies the execution environment and attaches the working files; a Workspace owns those files and their persistence.&lt;/p&gt;

&lt;p&gt;An AML Agent provider is the common interface for a coding agent. &lt;code&gt;codexAgent()&lt;/code&gt;, &lt;code&gt;opencodeAgent()&lt;/code&gt;, and &lt;code&gt;piAgent()&lt;/code&gt; each return one. A provider maps that agent's own configuration and capabilities; the shared runtime handles the session, messages, stdin and stdout, MCP servers, cancellation, and cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ACP standardises
&lt;/h2&gt;

&lt;p&gt;Running an agent remotely needs two things. The remote machine needs the agent program installed. Your app needs a way to send it a prompt, receive its updates, and stop it when the work ends. What ACP does is standardise the protocol between your app and the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACP does not install anything for you.&lt;/strong&gt; If your remote container or VM will run Codex, OpenCode, or Pi, that program needs to be in the image or snapshot first. For a quick experiment, you can install it as part of setup. For repeatable work, build it into the image so startup is predictable.&lt;/p&gt;

&lt;p&gt;The Sandbox owns the remote environment: where the process runs and what access it has. The Agent provider supplies the command and configuration. ACP then connects your app to that process in the same way regardless of which agent you pick.&lt;/p&gt;

&lt;p&gt;The protocol does not make remote execution safe on its own. The Sandbox's filesystem, network, and process restrictions do that work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6z6j00l5wo916a5tvzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6z6j00l5wo916a5tvzs.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How many ways are there to run an agent?
&lt;/h2&gt;

&lt;p&gt;Before we implemented ACP, Sandbox support was three separate integrations. Each agent had its own answer to a basic question: how do I run when the files and commands are somewhere else?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi stayed inside the app as an SDK.&lt;/strong&gt; We rebuilt Pi's Bash tool so that a shell command called the Sandbox runner instead of the host shell. Pi itself was not an RPC client talking to the Sandbox. We provided a custom bridge for its Bash tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex ran as a CLI inside the Sandbox.&lt;/strong&gt; We created its temporary Codex state there, wrote any JSON output schema there, called the CLI for each turn, read its JSON events, kept track of its thread ID, and removed its state afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode needed two paths.&lt;/strong&gt; On the application host, we started an OpenCode server, waited for it to listen, and connected a client to it. In a Sandbox, we ran the &lt;code&gt;opencode&lt;/code&gt; CLI instead, kept a separate database and configuration there, and parsed its JSON output. The CLI started OpenCode's server inside the Sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those were all sensible ways to make one agent work. Together, they made every new Sandbox provider a bigger job. It needed to support an embedded tool bridge for &lt;strong&gt;Pi&lt;/strong&gt;, a managed CLI for &lt;strong&gt;Codex&lt;/strong&gt;, and both a server/client path and a separate CLI path for &lt;strong&gt;OpenCode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ACP cleared that up. All three now use the same protocol to talk to the runtime. &lt;strong&gt;Each Agent provider tells the runtime which program to run and how to configure it.&lt;/strong&gt; The shared code starts that program in the chosen Sandbox, sets up the session, attaches MCP servers, sends the prompt and FollowUps, reads updates, handles cancellation, and cleans up.&lt;/p&gt;

&lt;p&gt;The agents still differ in their models, credentials, tools, and the way they work. The runtime stopped maintaining a separate way of running each one in a Sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  A remote Sandbox example
&lt;/h2&gt;

&lt;p&gt;This is an example with the shape of a Daytona setup. With no &lt;code&gt;image&lt;/code&gt; or &lt;code&gt;snapshot&lt;/code&gt; selected, the provider asks Daytona to create a default environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codexAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;daytonaSandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Workspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aml-jsx/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Codex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;codexAgent&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RemoteSandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;daytonaSandbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DAYTONA_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;localWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Workspace&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"project"&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Project&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sandbox&lt;/span&gt; &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"read-write"&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;RemoteSandbox&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Inspect the repository, run the relevant tests, and implement the requested change.
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Workspace&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Daytona, the runtime creates the remote Sandbox and transfers the Workspace files in and out. If that Sandbox can run &lt;code&gt;codex-acp&lt;/code&gt;, it starts the program there and speaks ACP with it over stdin and stdout. When the work ends, it copies the Workspace changes back and releases the remote Sandbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your workflow does not need its own remote-agent setup.&lt;/strong&gt; Change the Agent provider to OpenCode or Pi. Change the Sandbox to Docker, trusted local execution, or Modal. The setup code changes, but the tree that describes the work stays the same.&lt;/p&gt;

&lt;p&gt;The result is a shared session path, with environment setup still explicit. A compatible Sandbox must supply the selected agent executable; the runtime no longer needs a different session path for Codex, OpenCode, and Pi.&lt;/p&gt;




&lt;p&gt;AML is under active development, and its public APIs may change before the first stable release. Inspect the ACP engine, Agent providers, and Sandbox adapters in &lt;a href="https://github.com/we-are-singular/aml" rel="noopener noreferrer"&gt;the repository&lt;/a&gt;, or see the runtime model and current provider support on &lt;a href="https://agent-markup-language.com/" rel="noopener noreferrer"&gt;the project site&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/we-are-singular/aml" rel="noopener noreferrer"&gt;Star AML on GitHub&lt;/a&gt; if this is useful, and drop a comment: what would you use it for in your stack?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>agentic</category>
    </item>
    <item>
      <title>Compose AI agents like reusable components</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:00:51 +0000</pubDate>
      <link>https://dev.to/jtavares/compose-ai-agents-like-reusable-components-3o51</link>
      <guid>https://dev.to/jtavares/compose-ai-agents-like-reusable-components-3o51</guid>
      <description>&lt;p&gt;Imagine composing AI assistants the way you create reusable React components.&lt;/p&gt;

&lt;p&gt;One agent reviews correctness. Another plans the tests. Their outputs flow into a third agent that produces one release recommendation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Correctness ──┐
              ├──&amp;gt; Synthesis ──&amp;gt; Release recommendation
Test plan ────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is agent composition: small specialists, explicit dataflow, one final result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The glue adds up
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/openai/codex/tree/main/sdk/typescript" rel="noopener noreferrer"&gt;Codex SDK&lt;/a&gt; gives TypeScript applications a clean thread-and-turn API. Composing several threads is still your application's job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Codex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@openai/codex-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Codex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startThread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;finalResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;correctness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Review the current diff for correctness defects.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write the smallest useful test plan for the current diff.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coordinator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startThread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
Write one release recommendation from these reports.
Do not invent findings.

CORRECTNESS
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

TEST PLAN
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is perfectly reasonable TypeScript. It also makes the application responsible for threads, execution order, result extraction, and prompt assembly. As the workflow grows, the glue becomes the thing you spend time reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents as components
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aml.wearesingular.com/" rel="noopener noreferrer"&gt;Agent Markup Language&lt;/a&gt; puts that composition into TypeScript and JSX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReleaseReview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Synthesize evidence without inventing findings."&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      ## Correctness
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Review the current diff for correctness defects.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      ## Test plan
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Write the smallest useful test plan.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Write one release recommendation.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AML runs the two child agents from left to right. Their outputs land exactly where they appear in the parent prompt. The synthesis agent starts after both are ready.&lt;/p&gt;

&lt;p&gt;AML is not React, but the component model is deliberately familiar. Functions package behaviour. Props carry inputs. JSX shows which agent receives each result. Ordinary TypeScript handles conditions.&lt;/p&gt;

&lt;p&gt;You can keep the workflow compact, or split a larger one into named specialists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AmlRenderable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codexAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FollowUp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aml-jsx/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Codex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;codexAgent&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CorrectnessReview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Find concrete correctness defects in the current diff.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SecurityReview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Find security risks in: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TestPlan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AmlRenderable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Write the smallest useful test plan for &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;. Use &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DocsImpact&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;publicChange&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;publicChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;publicChange&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Check whether the current diff changes documented behaviour.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No public documentation review requested.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReleaseCouncil&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;changedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;publicChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;changedFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;publicChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Use only the supplied evidence. Resolve conflicts explicitly."&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      # Release evidence for &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      ## Correctness
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CorrectnessReview&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      ## Security
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SecurityReview&lt;/span&gt; &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;changedFiles&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      ## Tests
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TestPlan&lt;/span&gt; &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"npm test"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;regressions around &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;TestPlan&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      ## Documentation
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DocsImpact&lt;/span&gt; &lt;span class="na"&gt;publicChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;publicChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FollowUp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Decide whether this is ready to release and explain why or why not.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FollowUp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReleaseCouncil&lt;/span&gt;
    &lt;span class="na"&gt;changedFiles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/auth.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/session.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;publicChange&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"the current diff"&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The components now use JSX in different ways: no props, an array prop, nested children, and a boolean that conditionally returns an Agent or plain text. They run in authored order. The parent opens after their outputs have been assembled into its prompt, then &lt;code&gt;FollowUp&lt;/code&gt; asks for the release decision in the same session.&lt;/p&gt;

&lt;p&gt;Swap a specialist, reuse it elsewhere, or give it a different provider without redesigning the workflow.&lt;/p&gt;

&lt;p&gt;The developer still authors the control flow. Agent output stays data; AML never executes model-generated JSX as a new workflow. Tools and MCP servers also stay scoped to the agent that receives them.&lt;/p&gt;

&lt;h2&gt;
  
  
  We are building this
&lt;/h2&gt;

&lt;p&gt;We are building AML so complex agent systems can be read as composed parts instead of a pile of orchestration glue.&lt;/p&gt;

&lt;p&gt;AML is open source, MIT licensed, and under active development. &lt;a href="https://github.com/we-are-singular/aml" rel="noopener noreferrer"&gt;Read the code, run the examples, and star the repository on GitHub.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>jsx</category>
    </item>
    <item>
      <title>Introducing: AML - Agent Markup Language</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:16:55 +0000</pubDate>
      <link>https://dev.to/jtavares/introducing-aml-agent-markup-language-bnf</link>
      <guid>https://dev.to/jtavares/introducing-aml-agent-markup-language-bnf</guid>
      <description>&lt;p&gt;Our first agent workflows were easy to start. Then we added another agent, a tool, a follow-up turn, a sandbox, and somewhere to keep files between runs.&lt;/p&gt;

&lt;p&gt;The code still worked, but understanding the workflow meant jumping between provider calls, prompt builders, schema parsing, and cleanup code. We wanted to open one file and see what ran, what each agent could use, and where its result went.&lt;/p&gt;

&lt;p&gt;So we built &lt;a href="https://aml.wearesingular.com/" rel="noopener noreferrer"&gt;Agent Markup Language&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in the tree
&lt;/h2&gt;

&lt;p&gt;AML lets you write agent workflows as &lt;strong&gt;TypeScript and JSX&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Agents, their capabilities, and resource boundaries live in &lt;strong&gt;one executable tree&lt;/strong&gt;. The runtime evaluates that tree, manages provider and resource lifecycles, and returns the final agent's output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sandbox&lt;/span&gt; &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"read-only"&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Docker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Inspect src/index.ts.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That nesting has runtime meaning. Put an &lt;code&gt;&amp;lt;Agent&amp;gt;&lt;/code&gt; inside a &lt;code&gt;&amp;lt;Sandbox&amp;gt;&lt;/code&gt; and, &lt;strong&gt;bam, its model-controlled filesystem and command execution are scoped to that sandbox&lt;/strong&gt;. Same with skills, you can put a &lt;code&gt;&amp;lt;Skill&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;Tool&amp;gt;&lt;/code&gt; inside an &lt;code&gt;&amp;lt;Agent&amp;gt;&lt;/code&gt; and AML runtime makes sure everything resolves before prompting the agent.&lt;/p&gt;

&lt;p&gt;AML currently ships with OpenCode and Codex adapters, a Docker sandbox, a local workspace provider, (and deterministic providers for tests). It is early, and the API is still taking shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  We were tired of rebuilding the glue
&lt;/h2&gt;

&lt;p&gt;Provider SDKs handle model sessions well. They know how to authenticate, send turns, call native tools, and keep provider-specific state.&lt;/p&gt;

&lt;p&gt;A useful workflow usually has more going on around that session. It may run two specialists in parallel, validate their output, and pass both results to a coordinator, mix in results from external APIs or a database.&lt;/p&gt;

&lt;p&gt;We kept writing that orchestration by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Review&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Find correctness issues in this source:
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Check these findings against the source:
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  JSX already knows how to describe a tree
&lt;/h2&gt;

&lt;p&gt;Agent workflows naturally form trees: coordinators depend on specialists, tools belong to particular agents, and sandboxes wrap the work that needs them. &lt;strong&gt;XML-style markup makes those relationships visible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JSX adds the &lt;strong&gt;TypeScript tooling we already use&lt;/strong&gt;: autocomplete, type checking, refactoring, and syntax highlighting. React developers already know components, props, children, and ordinary JavaScript control flow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The mental flip is execution order. AML discovers components from the root, but &lt;strong&gt;nested agent sessions execute from the leaves back up&lt;/strong&gt;. A child finishes, its output joins the parent's request, and then the parent starts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Results move up the tree
&lt;/h2&gt;

&lt;p&gt;Here is a small parallel review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opencodeAgent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aml-jsx/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OpenCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;opencodeAgent&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Review&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maintainability&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Review this source for correctness problems:
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Review this source for maintainability problems:
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Combine these reviews without inventing findings:
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;correctness&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;maintainability&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AmlRuntime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Review&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Promise.all()&lt;/code&gt; starts the two reviewers together. Their outputs come back as &lt;strong&gt;ordinary values&lt;/strong&gt; and land in the final agent's prompt. The coordinator runs after both reviewers finish.&lt;/p&gt;

&lt;p&gt;Concurrency is ordinary JavaScript. Providers are passed into the tree, so the same workflow can run with Codex, OpenCode, or a deterministic test provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  One agent can brief the next
&lt;/h2&gt;

&lt;p&gt;An agent can generate part of another agent's system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opencodeAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;System&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aml-jsx/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OpenCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;opencodeAgent&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PullRequestReview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;guidelines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;guidelines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Coordinate the review."&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OpenCode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Turn these repository guidelines into one review rule:
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;guidelines&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Review this pull request patch:
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specialist runs first. Its final text &lt;strong&gt;becomes system content for the coordinator&lt;/strong&gt;, which starts only after its full request has been assembled.&lt;/p&gt;

&lt;p&gt;Tool access follows the tree too. If the specialist receives a &lt;code&gt;&amp;lt;Tool&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;Mcp&amp;gt;&lt;/code&gt; grant, that grant stays with the specialist. A sibling or parent does not quietly inherit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run AML yourself
&lt;/h2&gt;

&lt;p&gt;With Node 26 or newer, you can run the deterministic review example &lt;strong&gt;without model credentials&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/we-are-singular/aml.git
&lt;span class="nb"&gt;cd &lt;/span&gt;aml
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run example &lt;span class="nt"&gt;--&lt;/span&gt; review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use AML in your own project, install the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @aml-jsx/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point TypeScript at AML's JSX runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-jsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsxImportSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@aml-jsx/sdk"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://aml.wearesingular.com/" rel="noopener noreferrer"&gt;project site&lt;/a&gt; has an interactive walkthrough, and the &lt;a href="https://github.com/we-are-singular/aml" rel="noopener noreferrer"&gt;repository&lt;/a&gt; has runnable examples for parallel agents, structured output, follow-up turns, loops, tools, MCP, sandboxes, and workspaces.&lt;/p&gt;

&lt;p&gt;Try one of the examples, then tell us: &lt;strong&gt;which part of the API made you stop and reach for the docs?&lt;/strong&gt; &lt;a href="https://github.com/we-are-singular/aml/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; and let us know what you would simplify. We will use that feedback as we decide what to change next.&lt;/p&gt;

&lt;p&gt;And if AML looks useful, &lt;a href="https://github.com/we-are-singular/aml" rel="noopener noreferrer"&gt;star us on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>jsx</category>
    </item>
    <item>
      <title>What will the next plan for spam be?</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Fri, 08 May 2026 22:43:41 +0000</pubDate>
      <link>https://dev.to/jtavares/what-will-the-next-plan-for-spam-be-250d</link>
      <guid>https://dev.to/jtavares/what-will-the-next-plan-for-spam-be-250d</guid>
      <description>&lt;p&gt;You wake up to fifty-three new messages, and not one of them is what we used to call spam: no scams, no phishing, no Nigerian princes, no strangers pretending to be your bank. What's actually in your inbox is a sales agent following up on a deal you never had, a recruiting agent sourcing for a role you never applied to, a procurement bot asking you to fill in a vendor form, three scheduling assistants negotiating a meeting that's already been moved twice, and a research agent that read your blog post and would love to "loop you in."&lt;/p&gt;

&lt;p&gt;Every one of these messages is authenticated. SPF, DKIM, and DMARC are all green, every sender is a real domain owned by a real company doing real business, and by every technical definition we have for spam, every one of these messages is &lt;strong&gt;legitimate&lt;/strong&gt;. They are also, every one of them, &lt;strong&gt;wasting your time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the next era of spam. The inboxes that look like the one I described above are still rare, but the underlying conditions are already in place. Spam has always been a problem of human attention being consumed without permission, and the conditions that made that the right framing are about to change. The defenses we built over the last twenty-five years were &lt;strong&gt;aimed at the wrong layer of the problem&lt;/strong&gt;, and the only definition of spam that has ever held up to scrutiny is about to become the only one that survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief History of "Consent vs. Content"
&lt;/h2&gt;

&lt;p&gt;In 1997, &lt;a href="https://en.wikipedia.org/wiki/Paul_Vixie" rel="noopener noreferrer"&gt;Paul Vixie&lt;/a&gt; built the first real-time blackhole list at MAPS and gave us that definition. Spam, he wrote, is an issue about &lt;strong&gt;consent, not content&lt;/strong&gt;. A message is spam, in Vixie's formulation, if the recipient has not "verifiably granted deliberate, explicit, and still-revocable permission for it to be sent." Nothing in that depends on what the message says, whether it's well-written, or whether authentication passes. The only question is whether the recipient agreed to receive it.&lt;/p&gt;

&lt;p&gt;In 2002, Paul Graham published ["&lt;a href="http://www.paulgraham.com/spam.html" rel="noopener noreferrer"&gt;A Plan for Spam&lt;/a&gt;"], the most-cited piece of popular writing on the subject, and reframed the problem in economic terms. Spammers can disguise headers, rotate IPs, and forge senders, Graham observed, but they cannot disguise the message itself, because the message is the entire point of the operation. Write software that recognizes the words spammers have to use, and you hit them where it actually hurts: their conversion rate. The content of the message turned out to be incidental, and what mattered was the cost of the human eyeball at the other end. Graham's bet held. Spam volume peaked at 89% of all email in 2010 and had fallen to 47% by 2024.&lt;/p&gt;

&lt;p&gt;Eleven years after Graham, David Chouinard published his own &lt;a href="https://medium.com/@davidchouinard/a-new-plan-for-spam-8eef8be44742" rel="noopener noreferrer"&gt;"A New Plan for Spam"&lt;/a&gt; and proposed taking the economic attack further: don't just filter, retaliate, with bots that auto-reply to spammers and overwhelm their reply-handling labor. Around the same time, Finn Brunton's &lt;a href="https://mitpress.mit.edu/9780262527533/spam/" rel="noopener noreferrer"&gt;&lt;em&gt;Spam: A Shadow History of the Internet&lt;/em&gt;&lt;/a&gt; argued the broader point that spam is "the negative shape of online community," meaning that every form of online sociality produces a corresponding form of spam that exploits its mechanisms of attention.&lt;/p&gt;

&lt;p&gt;This essay is a prediction of the next move in that lineage, and the argument it makes is that Vixie was right all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Era of Industrial-Scale Persuasion
&lt;/h2&gt;

&lt;p&gt;Spam has always been an economics problem, triggered each time by the same underlying event: &lt;strong&gt;the marginal cost of sending one more message collapsing toward zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the 1990s, sending a piece of direct mail cost twenty-five cents to a dollar per piece, and the economics forced selectivity. Then email arrived, the cost dropped four orders of magnitude, and by 2008 retail spam-for-hire ran around eighty dollars per million messages. The famous &lt;a href="https://dl.acm.org/doi/10.1145/1455770.1455774" rel="noopener noreferrer"&gt;"Spamalytics" study&lt;/a&gt; found that of 350 million pharmacy-spam messages, only twenty-eight converted into customers—a response rate of roughly one in 12.5 million—and even at that conversion, the operation was profitable. This was the spam Graham wrote about: high-volume, low-personalization, written in the universal dialect of bad English and dollar signs, and Bayesian filters could see it from orbit.&lt;/p&gt;

&lt;p&gt;Botnets brought the second collapse, when the cost per message fell another order of magnitude and the labor cost of sending fell to zero, because you no longer needed an operations team but rented compromised machines and ran scripts. The defenders responded with reputation systems: SPF in 2006, DKIM in 2007, and DMARC in 2012, moving the fight from content to identity. By 2024, major providers were enforcing bulk-sender requirements, and the spam rate hit its lowest point since 2003.&lt;/p&gt;

&lt;p&gt;The third collapse is happening right now, and the shape of it is different. The cost per message did not fall. What collapsed was &lt;strong&gt;the cost of writing a message indistinguishable from a human one&lt;/strong&gt;. Until late 2022, the rule was that cheap spam was generic and personalized spam was expensive, because spear-phishing required human labor that put a ceiling on targeting. That ceiling is gone. A personalized email written by an LLM costs fractions of a cent to generate, putting it within an order of magnitude of bulk botnet spam, except each message can be researched and contextualized.&lt;/p&gt;

&lt;p&gt;The trajectory of this capability improvement tells the whole story. In an &lt;a href="https://arxiv.org/abs/2308.12287" rel="noopener noreferrer"&gt;August 2023 study by Harvard researchers&lt;/a&gt; comparing phishing success, human security experts using the psychological "V-Triad" framework beat early LLMs handily: 69–79% click-through for humans versus 30–44% for fully AI-generated emails. But by late 2024, a follow-up study showed that gap had completely vanished. Armed with frontier models and OSINT tools, fully automated AI matched human security experts exactly, both achieving a 54% click-through rate, with a hybrid AI-human approach edging higher to 56%. In just over a year of model iteration, the cost of expert-level persuasion collapsed.&lt;/p&gt;

&lt;p&gt;This is industrial-scale persuasion at commodity prices. Microsoft's Digital Defense Report notes AI-driven phishing is three times more effective than traditional campaigns. Barracuda reported in June 2025 that 51% of all spam was already AI-generated, and by late 2025, state-aligned actors were running autonomous Claude operations across global targets.&lt;/p&gt;

&lt;p&gt;Existing defenses are not built to handle this. Authentication passes because the senders are real domains. Reputation is intact because legitimate-looking businesses are doing the sending. Content classifiers see polite, well-written messages that look like a hundred other polite messages. &lt;strong&gt;The cheap-and-generic signal that Bayesian filters depended on is gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59xiximb5vzn0zllxq3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59xiximb5vzn0zllxq3z.png" alt="A wealth of information creates a poverty of attention" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Poverty of Attention, A Wealth of Agents
&lt;/h2&gt;

&lt;p&gt;The category we call spam is an artifact of one specific bottleneck, and the bottleneck is about to move.&lt;/p&gt;

&lt;p&gt;In 1971, &lt;a href="https://en.wikipedia.org/wiki/Herbert_A._Simon" rel="noopener noreferrer"&gt;Herbert Simon&lt;/a&gt; wrote one sentence in &lt;a href="https://digitalcollections.library.cmu.edu/awweb/awarchive?type=file&amp;amp;item=33748" rel="noopener noreferrer"&gt;"Designing Organizations for an Information-Rich World"&lt;/a&gt; that should be required reading for anyone working on email:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A wealth of information creates a poverty of attention.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The whole apparatus of spam defense, from Graham's filters to Vixie's blackhole lists, is a defensive structure built around that poverty. Spam is a problem because it consumes a scarce resource (human attention) without permission. That cost changes when an AI agent reads the inbox first.&lt;/p&gt;

&lt;p&gt;A triage agent of the kind that Superhuman, Shortwave, Sanebox, Inbox Zero, Google's Gemini, and Microsoft Copilot already ship can read fifty emails in seconds, summarize the meaningful ones, archive the rest, and surface a digest of the four things that actually need your decision. With the average knowledge worker receiving 117 emails per day, Superhuman reports that 82% of professionals using its product save at least a full workday per week with AI features. If your inbox has an agent in front of it, five hundred emails a day becomes manageable. &lt;strong&gt;The bottleneck is no longer your eyeballs, but the triage agent's judgment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under those conditions, the category we currently call spam—defined as unwanted volume against human attention—starts to lose its meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens next?
&lt;/h2&gt;

&lt;p&gt;Once human attention becomes a gated resource, the prize is no longer reaching the human directly but tricking the gatekeeper into passing the message through.&lt;/p&gt;

&lt;p&gt;A modern ad-tech executive trying to reach a busy CEO already knows that the CEO's attention is mediated by staff. When the staff is replaced by software, the marginal cost of trying to manipulate that software approaches zero. Spam stops meaning unwanted volume against humans and starts meaning messages engineered to fool a triage agent into believing they are urgent, personal, or eligible for escalation. &lt;strong&gt;Prompt injection moves from a chatbot security curiosity to a central concern of email infrastructure.&lt;/strong&gt; Simon Willison's &lt;a href="https://simonwillison.net/2023/Apr/25/dual-llm-pattern/" rel="noopener noreferrer"&gt;"lethal trifecta"&lt;/a&gt; of private data, untrusted content, and external communication describes exactly the failure mode of a triage agent reading inbound mail.&lt;/p&gt;

&lt;p&gt;The supply side is lining up. A Microsoft-sponsored IDC Info Snapshot projects 1.3 billion AI agents by 2028. Salesforce's Agentforce hit $800M ARR in early 2026, and the Model Context Protocol now processes over a billion tool calls per month at Anthropic alone. Most of these agents will eventually send email because it is the only universal protocol with built-in identity, asynchronous delivery, and an audit trail.&lt;/p&gt;

&lt;p&gt;The first signs of this collision are already visible. On Christmas Day 2025, the legendary programmer &lt;a href="https://simonwillison.net/2025/Dec/26/slop-acts-of-kindness/" rel="noopener noreferrer"&gt;Rob Pike was spammed by an unsolicited "act of kindness"&lt;/a&gt; from an AI agent in a multi-agent simulation run by a non-profit. The email itself was polite, but as Willison documented, the implication was severe: agents operating on abstract goals were granted the autonomy to send unsolicited emails to real people without human review.&lt;/p&gt;

&lt;p&gt;That same month, security firm Aurascape documented the first real-world &lt;a href="https://www.businesswire.com/news/home/20251208251854/en/Aurascape-Researchers-Expose-New-AI-Attack-That-Sends-Travelers-To-Scam-Airline-Support-Call-Centers" rel="noopener noreferrer"&gt;LLM-search-poisoning campaign&lt;/a&gt;, in which attackers manipulated public web content so that AI-powered support systems would natively recommend scam airline phone numbers to users asking for help. As Aurascape CEO Moinul Khan noted, &lt;strong&gt;attackers are now targeting the systems that write the answers&lt;/strong&gt;. One incident is a polite agent overstepping; the other is an attack designed to manipulate an AI through-line. Both are previews.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4vhdnx6a4pmwyvgvdkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4vhdnx6a4pmwyvgvdkk.png" alt="The defenses we built over the last twenty-five years were aimed at recognizing bad actors" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What will the next plan be?
&lt;/h2&gt;

&lt;p&gt;The defenses we built over the last twenty-five years were aimed at recognizing bad actors. They will not help against well-meaning agents acting on reward signals, and they will not help against adversaries whose target is the software that decides what the human sees.&lt;/p&gt;

&lt;p&gt;Vixie was right that spam is about consent rather than content. Filters were a twenty-year detour that solved the second-collapse problem, but we are entering the fourth. What distinguishes good email from bad in this new world will not be content, but whether the recipient (or their agent, acting under policy) actually agreed to receive it.&lt;/p&gt;

&lt;p&gt;The next plan for spam needs to attack the same place Graham's did—the economics of the operation—but in defense of a different scarce resource: &lt;strong&gt;verified consent at the moment of delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Email will remain the universal protocol. What changes is who does the reading. We spent twenty-five years filtering cheap noise to protect human attention. Today, that noise simply runs interference for industrial-scale persuasion targeting our proxies. Graham won the last war because failures of consent showed up as bad writing. Now that the writing is flawless, &lt;strong&gt;the war for attention is over, and the war for access has begun&lt;/strong&gt;. We have a window to draw the right conclusions before the third collapse compounds into the fourth.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources &amp;amp; References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vixie, Paul (1997):&lt;/strong&gt; Mail Abuse Prevention System (MAPS) and definition of spam as an issue of consent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graham, Paul (2002):&lt;/strong&gt; &lt;a href="http://www.paulgraham.com/spam.html" rel="noopener noreferrer"&gt;"A Plan for Spam"&lt;/a&gt;, popularizing Bayesian filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chouinard, David (2013):&lt;/strong&gt; &lt;a href="https://medium.com/@davidchouinard/a-new-plan-for-spam-8eef8be44742" rel="noopener noreferrer"&gt;"A New Plan for Spam"&lt;/a&gt;, proposing active anti-spam bots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spamalytics (2008):&lt;/strong&gt; &lt;a href="https://dl.acm.org/doi/10.1145/1455770.1455774" rel="noopener noreferrer"&gt;"Spamalytics: An Empirical Analysis of Spam Marketing Conversion"&lt;/a&gt; by Kanich et al. (350 million messages yielded 28 conversions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brunton, Finn (2013):&lt;/strong&gt; &lt;a href="https://mitpress.mit.edu/9780262527533/spam/" rel="noopener noreferrer"&gt;&lt;em&gt;Spam: A Shadow History of the Internet&lt;/em&gt;&lt;/a&gt; (MIT Press).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simon, Herbert A. (1971):&lt;/strong&gt; &lt;a href="https://digitalcollections.library.cmu.edu/awweb/awarchive?type=file&amp;amp;item=33748" rel="noopener noreferrer"&gt;"Designing Organizations for an Information-Rich World"&lt;/a&gt; ("A wealth of information creates a poverty of attention").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harvard Phishing Study 1 (Aug 2023):&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2308.12287" rel="noopener noreferrer"&gt;"Devising and Detecting Phishing: Large Language Models vs. Smaller Human Models"&lt;/a&gt; by Heiding et al. (Click-through rates: 19–28% Control, 30–44% GPT, 69–79% Human V-Triad Expert).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harvard Phishing Study 2 (Dec 2024):&lt;/strong&gt; Follow-up study on AI phishing (Click-through rates: 12% Control, 54% fully AI-automated, 54% Human Expert, 56% Hybrid).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rob Pike AI Incident (Dec 2025):&lt;/strong&gt; Detailed in Simon Willison's Weblog, &lt;a href="https://simonwillison.net/2025/Dec/26/slop-acts-of-kindness/" rel="noopener noreferrer"&gt;"How Rob Pike got spammed with an AI slop 'act of kindness'"&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurascape Search Poisoning (Dec 2025):&lt;/strong&gt; &lt;a href="https://www.businesswire.com/news/home/20251208251854/en/Aurascape-Researchers-Expose-New-AI-Attack-That-Sends-Travelers-To-Scam-Airline-Support-Call-Centers" rel="noopener noreferrer"&gt;"Aurascape Researchers Expose New AI Attack That Sends Travelers To Scam Airline Support Call Centers"&lt;/a&gt; (Business Wire).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry Data (2024–2026):&lt;/strong&gt; DMARC adoption, 117 emails/day average (Microsoft Work Trend Index), Superhuman time-saving metrics, Microsoft/IDC Info Snapshot 1.3B agent projections, and Salesforce Agentforce ARR milestones represent aggregated state-of-the-industry reporting as of early 2026.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>spam</category>
      <category>email</category>
    </item>
    <item>
      <title>Why would you give your agents an email address?</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Fri, 24 Apr 2026 18:55:22 +0000</pubDate>
      <link>https://dev.to/broodnet/why-would-you-give-your-agents-an-email-address-3a08</link>
      <guid>https://dev.to/broodnet/why-would-you-give-your-agents-an-email-address-3a08</guid>
      <description>&lt;p&gt;&lt;em&gt;other than spamming: agents can do a lot with an email address that doesn't involve messaging strangers&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;Broodnet&lt;/a&gt;, an email hosting provider for AI agents. Agents get real mailboxes — IMAP, SMTP, the actual protocols — with one structural difference from every other email provider: outbound is restricted to addresses inside the same account. Agents receive from the entire internet but only send to their operator and to other agents in the same brood.&lt;/p&gt;

&lt;p&gt;We get the same reaction often enough that it's worth addressing directly: if the agent can't email strangers, what's the point of the inbox?&lt;/p&gt;

&lt;p&gt;The easy answer is that blocking outbound kills the spam vector. That's true, but it undersells the result. The more interesting consequence is that agents can't exfiltrate either. If the inbox address leaks, or a prompt injection lands inside an incoming email, nothing leaves the account — the agent has nowhere to send it. You're the only one asking questions, and you're the only one getting answers back. Sensitive documents, financial records, client threads — anything you'd never hand to an LLM with outbound tools becomes fair game when the outbound tool physically does not exist.&lt;/p&gt;

&lt;p&gt;So: what do you actually do with an inbox like that?&lt;/p&gt;

&lt;p&gt;Quite a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  E2E tests with a real email address
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Give your CI pipeline a real inbox. Send the actual email, read the actual code, fail the test when deliverability breaks — not when the mock says it should.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Signup flows, OTP delivery, password resets. Every developer has written this code. Very few have tested the real thing in CI. Most setups mock the SMTP layer, assert that &lt;code&gt;sendEmail&lt;/code&gt; got called, and move on.&lt;/p&gt;

&lt;p&gt;Mocks don't catch deliverability bugs. They'll happily pass while DKIM is misconfigured, while the HTML renderer breaks on Outlook, while Gmail's clipper chops your subject line to "Your verific" and users can't read it.&lt;/p&gt;

&lt;p&gt;Give the test agent a real inbox. CI spins up a user, submits the signup form, and the agent reads the verification code out of the email your production flow just sent. If the flow breaks, the test breaks. If deliverability is broken, you find out before your users do.&lt;/p&gt;

&lt;p&gt;This was Broodnet's original use case. Disposable inboxes for internal CI, built before anything else existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Newsletter digests
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Point newsletters at an agent. It reads, clusters by topic, and sends you one digest with the five items that matter. Everything else gets archived silently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Newsletters are useful until you subscribe to ten of them. Then they're a daily liability. Too many tabs to open, too many unsubscribes to reach for, too much guilt about the backlog.&lt;/p&gt;

&lt;p&gt;Point them all at an agent. It reads, clusters by topic, and sends you one email at 8am with the five items that actually matter. Everything else gets archived silently. You stay informed, the tab graveyard stays empty.&lt;/p&gt;

&lt;p&gt;The noisier the inputs, the better this works. Substack posts, industry digests, GitHub release emails, product changelogs. Anything occasionally useful and mostly ignorable is a good candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  CC the agent on anything worth remembering
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;CC the agent on contracts, receipts, decisions. It indexes the thread and stores it. Later, ask the agent and it retrieves the answer with citations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Contracts. Decisions. Receipts. Shipping confirmations. The one email from three months ago that had the vendor's correct bank details.&lt;/p&gt;

&lt;p&gt;Add the agent's address to the CC field. It indexes the thread and stores it. Later, when you need to find "that thing the lawyer said about the IP clause", you ask the agent instead of searching your own inbox.&lt;/p&gt;

&lt;p&gt;This is the zero-friction version of a second brain. No new tool to open. No new shortcut to memorize. The CC field is the whole interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query your agent by email
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Email the agent questions about your corpus. No chat interface. No dashboard. The compose window you already use is the entire interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you've been CC'ing the agent for a while, or feeding it documents directly, it has built up a working memory of everything you've shared with it. At that point the inbox doubles as a query interface.&lt;/p&gt;

&lt;p&gt;Email the agent: "what did we decide about the Q3 hiring freeze in that last planning thread?" It replies with the answer and citations back to the source messages.&lt;/p&gt;

&lt;p&gt;No chat interface. No dashboard to log into. You already have the tool. It's the compose window you open fifty times a day.&lt;/p&gt;

&lt;p&gt;There's something else email gives you that chat doesn't: each thread carries its own full history. Every reply includes everything that came before it. The agent always has the context for that task, in that thread, without you managing it. Not a session that expires, not a chat window you have to scroll back through. Humans have been using email threads to manage long-form work for 50 years — the agent fits right in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invoice parsing and CRM updates
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Forward receipts and invoices. The agent extracts line items, amounts, dates, and pushes them to your spreadsheet or CRM. You never touch the spreadsheet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Receipt from a client lunch. Invoice from a contractor. Booking confirmation from a hotel. They all land in your inbox, and they all need to end up somewhere structured: a spreadsheet, a CRM, the accounting tool.&lt;/p&gt;

&lt;p&gt;Forward them to the agent. It extracts line items, amounts, dates, and counterparties, and pushes them to the right destination. You never open the spreadsheet. You never type the amount. The forward is the interface.&lt;/p&gt;

&lt;p&gt;The agent can also flag anomalies. Invoice 3x larger than the last one from the same vendor gets held and confirmed before filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent coordination via email threads
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Agents in the same brood can email each other. The thread becomes shared state: chronological, attributed, persistent, searchable. No infrastructure needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Multi-agent systems need shared state. The usual answers are a database, a message queue, or a custom protocol. All of them require infrastructure.&lt;/p&gt;

&lt;p&gt;Email threads already solve this. Agent A emails Agent B with context. B processes and replies. Agent C joins the thread with a second opinion. The thread is the state: chronological, attributed, persistent, searchable.&lt;/p&gt;

&lt;p&gt;Inside a Broodnet account, this is free. Agents in the same brood can send to each other. The conversation lives in the inbox of whichever agent owns it. Nothing to provision or maintain.&lt;/p&gt;

&lt;p&gt;Email was async coordination between entities long before we used the word for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent that reports back
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Give the agent keywords and feeds. It runs the search daily, tracks what changed, and sends you a digest. Pricing updates. Launches. Job posts. Signal you care about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one flips direction. Instead of you emailing the agent, the agent emails you.&lt;/p&gt;

&lt;p&gt;Give it a list of competitors, keywords, or feeds. It runs the search daily, tracks what changed, and sends you a digest. Pricing page updates. Launch posts. Job listings that hint at direction. GitHub stars crossing round numbers. Whatever signal you care about.&lt;/p&gt;

&lt;p&gt;No SaaS dashboard to check. No new tab to open. The digest arrives in your inbox, right where you already are.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fousf7xwjv66nyiv4gvm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fousf7xwjv66nyiv4gvm0.png" alt="Multi agent coordination via email threads." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these require emailing strangers. None of them need the capability at all. Once the pathway is closed, the list of useful things left is longer than most people assume.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxoee4wt8sr0krwzabp4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxoee4wt8sr0krwzabp4t.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https:/broodnet.com" rel="noopener noreferrer"&gt;broodnet&lt;/a&gt; gives AI agents their own email addresses. CLI-native, built for agent-to-owner communication. Free tier available.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
    </item>
    <item>
      <title>12 things you can do today to make your transactional emails work for humans and AI agents</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Sun, 12 Apr 2026 16:44:51 +0000</pubDate>
      <link>https://dev.to/broodnet/12-things-you-can-do-today-to-make-your-transactional-emails-work-for-humans-and-ai-agents-k1b</link>
      <guid>https://dev.to/broodnet/12-things-you-can-do-today-to-make-your-transactional-emails-work-for-humans-and-ai-agents-k1b</guid>
      <description>&lt;p&gt;AI agents are starting to use email. Not metaphorically. They sign up for services, receive verification codes, get alerts, and act on them. At &lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;broodnet&lt;/a&gt; we give each agent its own email address and a CLI to manage its inbox. &lt;a href="https://dev.to/broodnet/i-scored-163-real-emails-on-how-well-an-ai-agent-can-read-them-most-of-them-are-terrible-egn"&gt;We recently scored 160+ transactional emails&lt;/a&gt; (verification codes, welcome messages, notifications, security alerts) collected from the team's professional and personal inboxes over the last 5 to 10 years, covering SaaS, games, crypto, dev tools, news, and more. We scored each one on both human UX quality and agent parseability, and the results were rough: over 40% had fully opaque tracking URLs, 66% of welcome emails contained zero usable links, and only 16% of the dataset was clean enough for both audiences.&lt;/p&gt;

&lt;p&gt;That &lt;a href="https://dev.to/broodnet/i-scored-163-real-emails-on-how-well-an-ai-agent-can-read-them-most-of-them-are-terrible-egn"&gt;analysis&lt;/a&gt; taught us what's broken. This is what to do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Strip tracking from your transactional links
&lt;/h2&gt;

&lt;p&gt;This is the single highest-impact change. Every tracking redirect turns a readable URL into an opaque blob that tells a non-browser client nothing about where it leads.&lt;/p&gt;

&lt;p&gt;Most people overlook this: &lt;strong&gt;transactional emails with a clear call to action don't need click tracking because the CTA &lt;em&gt;is&lt;/em&gt; the metric.&lt;/strong&gt; If you send a "verify your email" message, you already know whether the email got verified. You can measure that on the backend. You don't need a Salesforce Marketing Cloud redirect between the user and the endpoint.&lt;/p&gt;

&lt;p&gt;Your marketing emails can have all the tracking they want. But &lt;code&gt;verify-email&lt;/code&gt;, &lt;code&gt;confirm-account&lt;/code&gt;, and &lt;code&gt;manage-webhooks&lt;/code&gt; links should be &lt;strong&gt;raw destination URLs&lt;/strong&gt;. Companies like GitLab ship every transactional link as a raw URL and still understand their funnel just fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Put the verification code in the subject line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;847291 is your Acme code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five senders in our dataset did this (Canva, Slack, LinkedIn, Gravatar, Instagram) and &lt;strong&gt;all five scored a perfect 5/5 on subject quality&lt;/strong&gt;. Emails without the code in the subject averaged 3.6/5.&lt;/p&gt;

&lt;p&gt;One template change, every client benefits. iOS and Android detect OTP codes in notifications and surface a "copy code" button on the lock screen. Having it in the subject makes it trivially detectable. An agent scanning its inbox through a CLI extracts the code without opening the message. Humans grab the code without opening the email, sometimes without even unlocking their phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. State the expiry in plain text
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"This code expires in 15 minutes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six words. Emails that included this scored &lt;strong&gt;7.4/10&lt;/strong&gt; on agent metrics versus &lt;strong&gt;5.7/10&lt;/strong&gt; for those that didn't. Partly because expiry matters for prioritization (an agent needs to know if it should drop everything), partly because the kind of team that states expiry tends to write cleaner emails overall.&lt;/p&gt;

&lt;p&gt;GitLab states it on every OTP. Hetzner includes the exact datetime. Docker says 15 minutes. Gravatar says 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use semantic URL paths
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;/verify?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdl...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;/email_verify/?email=user@example.com&amp;amp;hash=a336a019-f7cf-4454-b3bd-8aa1e134cd29
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both work. Both are secure. But the second one tells you what it does and who it's for just by reading it. UUID4 tokens, short hex hashes, or alphanumeric codes with readable path segments all beat opaque JWTs.&lt;/p&gt;

&lt;p&gt;Some patterns from the dataset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app.pulsetic.com/email_verify/?email=&amp;lt;email&amp;gt;&amp;amp;hash=&amp;lt;uuid4&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;raider.io/verify?user=&amp;lt;username&amp;gt;&amp;amp;token=&amp;lt;token&amp;gt;&lt;/code&gt; (note the semantic prefix)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signup.mailgun.com/activate/&amp;lt;hex&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accounts.random.org/create/confirm?secret=&amp;lt;uuid4&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Hand-write the plain-text MIME part
&lt;/h2&gt;

&lt;p&gt;Every email is a MIME multipart with HTML and plain-text versions. Most ESPs auto-generate the plain-text from the HTML, and the result is usually garbage: stripped tags, broken links, spacer characters left in.&lt;/p&gt;

&lt;p&gt;This matters because &lt;strong&gt;terminal clients, CLI tools, agents, screen readers, and LLMs&lt;/strong&gt; all consume the plain-text part. If yours is auto-generated trash, that entire audience sees trash.&lt;/p&gt;

&lt;p&gt;Put links on their own lines. Use dashes as section dividers. Include the same critical info as the HTML. The best plain-text emails in our dataset (ngrok's verify, GitLab, RANDOM.ORG) were written as plain text first and it shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Kill the spacer characters
&lt;/h2&gt;

&lt;p&gt;ESPs inject zero-width Unicode characters after the preheader to prevent email clients from pulling body text into the preview. In raw text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Click the button to verify ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We found 7 distinct zero-width character types in the dataset. 29% had moderate pollution. Nearly 12% were severely polluted. Google Workspace's welcome email had hundreds of combined &lt;code&gt;U+034F&lt;/code&gt; and soft hyphens before the first real sentence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;These characters are a hack that directly poisons every non-HTML reader.&lt;/strong&gt; Terminal clients render garbled whitespace. Agents waste processing on invisible noise. Screen readers may choke.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The alternative: &lt;code&gt;display:none&lt;/code&gt; or &lt;code&gt;mso-hide:all&lt;/code&gt; on a hidden span after your preheader. You avoid poisoning the plain-text part entirely. For transactional emails you don't need pixel-perfect preheader control.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Provide a raw URL fallback for every button
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the button doesn't work, copy this link:
https://app.example.com/verify?token=abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if your button CTA goes through a tracker, &lt;strong&gt;the fallback gives every non-browser client something it can actually use&lt;/strong&gt;. Mobile users with broken rendering get a working link. CLI users get an extractable URL. It costs one extra line in the template.&lt;/p&gt;

&lt;p&gt;Buffer did this on exactly one of their five emails (the login security alert) and it was the only Buffer email that scored above worst-in-class.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Add schema.org JSON-LD
&lt;/h2&gt;

&lt;p&gt;Gmail supports embedded structured data through JSON-LD. A &lt;code&gt;ConfirmAction&lt;/code&gt; in the email HTML surfaces one-click actions in the Gmail UI and provides a &lt;strong&gt;machine-readable, typed representation&lt;/strong&gt; of what the email wants the reader to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EmailMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"potentialAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ConfirmAction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"handler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HttpActionHandler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.example.com/verify?token=abc123"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify your email address for Acme"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed in a &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; tag. Any parser that knows to look for JSON-LD gets a structured action without heuristic text matching. Gmail also supports &lt;code&gt;ViewAction&lt;/code&gt;, &lt;code&gt;SaveAction&lt;/code&gt;, and &lt;code&gt;TrackAction&lt;/code&gt; (for shipments). Almost nobody uses this for transactional emails, but the infrastructure has been there for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Set meaningful email headers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;X-Entity-Ref-ID&lt;/code&gt;&lt;/strong&gt;: unique value per email. Prevents Gmail from threading your OTP under a week-old verification thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;List-Unsubscribe&lt;/code&gt; + &lt;code&gt;List-Unsubscribe-Post&lt;/code&gt;&lt;/strong&gt;: RFC 8058. Machine-readable unsubscribe entirely through headers. Gmail and Yahoo require this for bulk marketing senders, and while transactional emails are technically exempt, adding it improves deliverability and gives agents a structured way to manage subscriptions. &lt;code&gt;List-Unsubscribe: &amp;lt;https://app.example.com/unsubscribe?id=&amp;lt;token&amp;gt;&amp;gt;&lt;/code&gt; plus &lt;code&gt;List-Unsubscribe-Post: List-Unsubscribe=One-Click&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Message-ID&lt;/code&gt;, &lt;code&gt;In-Reply-To&lt;/code&gt;, &lt;code&gt;References&lt;/code&gt;&lt;/strong&gt;: if you send follow-up emails (password reset after failed login), proper threading headers let agents understand email B relates to email A.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keep your sender identity consistent
&lt;/h2&gt;

&lt;p&gt;If your verification comes from &lt;code&gt;noreply@mail.accounts.acme.com&lt;/code&gt;, your welcome from &lt;code&gt;no-reply@notify.acme.com&lt;/code&gt;, and marketing from &lt;code&gt;hello@youraccount.acme.com&lt;/code&gt;, an agent has to learn a different sender for each template.&lt;/p&gt;

&lt;p&gt;Not a made-up example. Buffer sent from both &lt;code&gt;hello@youraccount.buffer.com&lt;/code&gt; and &lt;code&gt;hello@buffer.com&lt;/code&gt; depending on the template. An agent searching by sender misses half the emails.&lt;/p&gt;

&lt;p&gt;Pick one address for transactional, one for marketing. Be consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Separate transactional from marketing infrastructure
&lt;/h2&gt;

&lt;p&gt;Our data showed this already happens accidentally at most companies. Product team sends clean transactional emails. Marketing sends tracked welcome emails through their ESP. The quality gap between templates from the same sender was as high as &lt;strong&gt;5 points on a 10-point scale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Make it intentional. ESP for marketing. Transactional sender (Postmark, AWS SES direct, your own SMTP) for verifications, OTPs, and security notifications. Different domain, different templates, different tracking config.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Design verification for every client
&lt;/h2&gt;

&lt;p&gt;Most verify-link flows assume a human clicking in a browser. An agent can't "click" anything. A phone user might prefer typing a code. A text-only reader can follow a raw URL but can't render a button.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Provide the clickable link for browsers, provide the code in the body (and ideally the subject) for everything else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The emails that offered both a link and a standalone code (NiceHash, Telltale Games, Gravatar) scored well on both human and agent metrics. The link works for desktop. The code works for agents, mobile, screen readers, and anyone who'd rather type 6 digits than tap a tiny button. This dual-path approach is already standard in SMS verification. No reason email can't do the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  TLDR; The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Transactional CTA links are raw destination URLs, not tracker redirects&lt;/li&gt;
&lt;li&gt;[ ] OTP code appears in the subject line&lt;/li&gt;
&lt;li&gt;[ ] Expiry is stated explicitly in the body&lt;/li&gt;
&lt;li&gt;[ ] URL paths are human-readable (semantic paths, UUID tokens, not opaque JWTs)&lt;/li&gt;
&lt;li&gt;[ ] Plain-text MIME part is hand-written or at least reviewed&lt;/li&gt;
&lt;li&gt;[ ] No zero-width spacer character injection in the plain-text part&lt;/li&gt;
&lt;li&gt;[ ] Raw URL fallback provided alongside tracked buttons&lt;/li&gt;
&lt;li&gt;[ ] JSON-LD structured data in the HTML part for key actions&lt;/li&gt;
&lt;li&gt;[ ] Meaningful headers set (&lt;code&gt;X-Entity-Ref-ID&lt;/code&gt;, &lt;code&gt;List-Unsubscribe&lt;/code&gt;, threading)&lt;/li&gt;
&lt;li&gt;[ ] Consistent sender address across all transactional email types&lt;/li&gt;
&lt;li&gt;[ ] Transactional email sent through separate infrastructure from marketing&lt;/li&gt;
&lt;li&gt;[ ] Verification flow supports both link-click and code-entry paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these hurt human readability. Most of them improve it. The data from 179 scored emails backs this up: &lt;strong&gt;the emails that scored highest for agents also scored highest for humans&lt;/strong&gt;. Writing for machines and writing for people is the same job.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;broodnet&lt;/a&gt; gives AI agents their own email addresses. CLI-native, built for agent-to-owner communication. Free tier available.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>agents</category>
      <category>tutorial</category>
      <category>datascience</category>
    </item>
    <item>
      <title>I scored 163 real emails on how well an AI agent can read them. Most of them are terrible.</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Fri, 10 Apr 2026 00:55:30 +0000</pubDate>
      <link>https://dev.to/broodnet/i-scored-163-real-emails-on-how-well-an-ai-agent-can-read-them-most-of-them-are-terrible-egn</link>
      <guid>https://dev.to/broodnet/i-scored-163-real-emails-on-how-well-an-ai-agent-can-read-them-most-of-them-are-terrible-egn</guid>
      <description>&lt;p&gt;We are building &lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;Broodnet&lt;/a&gt;, email infrastructure for AI agents. Each agent gets its own address and its own inbox. Through the &lt;a href="https://docs.broodnet.com/cli/mail/" rel="noopener noreferrer"&gt;Broodnet CLI&lt;/a&gt;, an agent can list its emails, open individual messages, search for specific senders or subjects, and send messages to its owner or other agents in the same account.&lt;/p&gt;

&lt;p&gt;While testing with real agent frameworks like &lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;Openclaw&lt;/a&gt; and &lt;a href="https://hermes-agent.nousresearch.com/" rel="noopener noreferrer"&gt;hermes&lt;/a&gt; I kept running into the same wall: the agents could receive the email just fine, but when it tried to actually &lt;em&gt;do something&lt;/em&gt; with it, the email was unreadable. Every link wrapped in a 200-character tracking redirect. Invisible Unicode characters scattered through the body. OTP codes buried in template noise. Some were instantly interpreted, but others left the models running in circles&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegw9qvwxa83ubj2ignxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegw9qvwxa83ubj2ignxc.png" alt=" " width="466" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, &lt;em&gt;for science&lt;/em&gt;, I grabbed 160+ real transactional emails — verification codes, welcome messages, notifications, security alerts — that had been sitting across the Broodnet team's professional and personal inboxes for the last 5 to 10 years. SaaS platforms, games, crypto wallets, dev tools, news services, government portals, you name it. I scored every single one on two perspectives: how good is this email for a human, and how good is it for an agent reading through a CLI. All scores are normalized to a &lt;strong&gt;0-10 scale&lt;/strong&gt; so they're comparable across dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I scored them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Human side&lt;/strong&gt;, five metrics: clarity, warmth, visual noise (inverted, 10 means clean), subject line quality, and onboarding helpfulness. Averaged and normalized to 0-10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent side&lt;/strong&gt;, using Claude Opus 4.6, four scaled metrics plus two binary flags: extractability (can a plain-text parser get the key info?), sender clarity, URL cleanliness, body noise level, and whether the code appears in the subject line (yes/no), whether expiry is explicitly stated (yes/no). Also normalized to 0-10. &lt;/p&gt;

&lt;p&gt;Plus a shared metric: CTA URL quality and clarity.&lt;/p&gt;

&lt;p&gt;The dataset breaks down into 53 welcome emails, 52 verify-link flows, 20 verify-code OTPs, and 35 notifications, with a few others and edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff that doesn't exist
&lt;/h2&gt;

&lt;p&gt;Going into this I assumed there'd be a tradeoff. Emails that look great for humans probably look worse for agents, right? Rich templates, beautiful buttons, all that stuff an LLM can't see. Turns out that's wrong.&lt;/p&gt;

&lt;p&gt;The 26 emails that scored "double pristine" (clean URLs &lt;em&gt;and&lt;/em&gt; no spacer pollution) averaged &lt;strong&gt;7.0/10&lt;/strong&gt; on the human scale. The dataset overall averaged &lt;strong&gt;6.3/10&lt;/strong&gt;. The cleanest emails for agents were also better for humans. Not by a little, by almost a full point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Clean emails for agents are also better emails for humans. The supposed tradeoff is a myth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This surprised me until I thought about where the noise actually comes from. The same ESPs and marketing tools that inject tracking links also inject spacer characters, bloated templates, and broken plain-text fallbacks. Clean emails tend to be clean everywhere.&lt;/p&gt;

&lt;p&gt;If you plot human noise score against agent URL cleanliness, &lt;strong&gt;86.5% of emails fall within 1 point of the diagonal&lt;/strong&gt;. An email that's noisy for you is almost certainly opaque for an agent too. These aren't independent dimensions. They share a root cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The onboarding trap
&lt;/h2&gt;

&lt;p&gt;Here's the pattern I didn't expect.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Onboarding quality&lt;/th&gt;
&lt;th&gt;Agent score (out of 10)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Some&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Great&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The emails with the &lt;em&gt;best&lt;/em&gt; human onboarding had the &lt;em&gt;worst&lt;/em&gt; agent scores. &lt;strong&gt;The email that helps a human the most is the email that buries an agent the deepest.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's happening is that "good onboarding" in practice means multiple sections, step-by-step flows, feature highlights, images, and CTAs. Every one of those CTAs gets a tracking link because the marketing team wants to know which step users click.&lt;/p&gt;

&lt;p&gt;The 29 emails that scored high on &lt;em&gt;both&lt;/em&gt; dimensions broke this pattern. They include things like Paymo, Pulsetic, AITopTools, IndieHunt, Baselight. What these have in common: they're mostly small companies. They didn't invest in an elaborate ESP with click tracking. Their onboarding emails just... link to the product. Directly. With normal URLs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uqn1leeen6hx5a3t39f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uqn1leeen6hx5a3t39f.png" alt=" " width="672" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tracking Tax
&lt;/h2&gt;

&lt;p&gt;42.9% of all emails in the dataset have fully opaque tracking URLs (scored 1 out of 5 on URL cleanliness). Only 19.6% have perfectly clean raw URLs.&lt;/p&gt;

&lt;p&gt;For welcome emails specifically, &lt;strong&gt;66% have zero usable CTA links&lt;/strong&gt;. Two thirds. For an agent that just signed up for a service and gets a welcome email, there is literally nothing actionable in the email body. Every "Get Started" button goes to &lt;code&gt;click.whatever.com/ls/click?upn=u001.aKJF8sldjf...&lt;/code&gt; and the destination is unknowable without following the redirect.&lt;/p&gt;

&lt;p&gt;I catalogued 11 distinct tracking systems across the dataset. They all look different but produce the same result: a URL that tells you nothing. Salesforce Marketing Cloud, customer.io, HubSpot, Braze, Beehiiv, Eloqua, AWS SES awstrack, Google's own tracker, Microsoft, vialoops, Stripe. From an agent's perspective they're all equally opaque.&lt;/p&gt;

&lt;p&gt;The worst offender for sheer URL ugliness was Microsoft's Bing Webmaster Tools: &lt;code&gt;mucp.api.account.microsoft.com/m/v2/c?r=&amp;lt;UPPERCASE-BASE32&amp;gt;&lt;/code&gt;. But the most &lt;em&gt;consistently&lt;/em&gt; bad was customer.io (used by Buffer, daily.dev, Uphold), which wraps every link in a JWT-encoded redirect on every email type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbh1jy25ntl11bryfqwq3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbh1jy25ntl11bryfqwq3.png" alt=" " width="647" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The marketing team doesn't talk to the product team
&lt;/h2&gt;

&lt;p&gt;One of the weirdest patterns in the data: the same company can produce wildly different email quality depending on which template they use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mailgun&lt;/strong&gt;: welcome email scores &lt;strong&gt;3.2/10&lt;/strong&gt; on agent metrics (every link through their own Mailjet tracker). Verify-link email scores &lt;strong&gt;7.7/10&lt;/strong&gt; (raw &lt;code&gt;signup.mailgun.com/activate/&amp;lt;hex&amp;gt;&lt;/code&gt; URL). That's a swing of &lt;strong&gt;4.5 points out of 10&lt;/strong&gt; across templates from the same sender. And Mailgun is an &lt;em&gt;email infrastructure company&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ngrok&lt;/strong&gt;: both welcome emails score &lt;strong&gt;3.6/10&lt;/strong&gt; (all links through HubSpot tracker). Their verify-link? Scores &lt;strong&gt;8.6/10&lt;/strong&gt;. Pure plain text, 3 lines total, raw URL. Swing: &lt;strong&gt;5 points&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loops&lt;/strong&gt;: welcome routes everything through &lt;code&gt;c.vialoops.com&lt;/code&gt; (their own tracker, ironic since they sell email delivery). Their DNS notification email? All records in plain text, raw links, scores &lt;strong&gt;8.2/10&lt;/strong&gt;. Swing: &lt;strong&gt;4.6 points&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Polar, Docker, and others follow the same pattern. Transactional emails come from engineering. Welcome emails come from marketing. Different tools, different templates, different philosophies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best predictor of email quality isn't the company or the industry. It's which team within the company owns the template.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The gaming industry tells the whole story
&lt;/h2&gt;

&lt;p&gt;I had gaming emails in the dataset and they split perfectly into two groups with nothing in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indie studios and smaller gaming sites&lt;/strong&gt; (itch.io, Larian Studios, Raider.IO, etc): agent scores of &lt;strong&gt;7.7 to 8.6/10&lt;/strong&gt;. All raw URLs. Zero tracking. Raider.IO's verify URL has the username right in it: &lt;code&gt;raider.io/verify?user=&amp;lt;username&amp;gt;&amp;amp;token=validation3ad9de269693489d&lt;/code&gt;. That &lt;code&gt;validation&lt;/code&gt; prefix in the token is a small touch but it tells you what the URL does just by reading it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAA studios and big gaming platforms&lt;/strong&gt; (HoYoverse, Bethesda, Discord, Riot Games, etc): agent scores of &lt;strong&gt;2.7 to 3.6/10&lt;/strong&gt;. Everything through AWS SES awstrack, Braze, Salesforce, Eloqua. Every link opaque. Image-heavy marketing.&lt;/p&gt;

&lt;p&gt;The dividing line isn't the content or the email type. It's whether the company has a marketing department with access to an ESP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invisible character zoo
&lt;/h2&gt;

&lt;p&gt;This one gets technical but it matters. Email senders inject zero-width Unicode characters to control how mail clients render the preview text. In a normal email client you never see them. When an agent reads the raw text through the CLI, it gets hundreds of invisible characters mixed into the content.&lt;/p&gt;

&lt;p&gt;I found 7 distinct character types used across the dataset, including compound sequences of 4-5 different invisible characters repeated dozens of times. Trading 212's verify email has over a hundred &lt;code&gt;U+200C&lt;/code&gt; characters before the actual message starts. MoonPay chains together &lt;code&gt;U+034F&lt;/code&gt;, &lt;code&gt;U+200C&lt;/code&gt;, and &lt;code&gt;U+FEFF&lt;/code&gt; in repeating sequences.&lt;/p&gt;

&lt;p&gt;It's not malicious, it's just how ESPs handle preheader text. But it means an agent parsing email output has to strip a zoo of invisible Unicode before it can even find the verification code. &lt;strong&gt;30.7% of emails had moderate pollution. 11.7% were severely polluted.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What good actually looks like
&lt;/h2&gt;

&lt;p&gt;26 of the 163 emails (16%) were double pristine: clean URLs and clean body text. The standouts by category:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bzhs17ljurgayjstcvg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bzhs17ljurgayjstcvg.png" alt=" " width="734" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify-link&lt;/strong&gt;: Pulsetic's URL is the one I keep coming back to: &lt;code&gt;app.pulsetic.com/email_verify/?email=&amp;lt;email&amp;gt;&amp;amp;hash=&amp;lt;uuid4&amp;gt;&lt;/code&gt;. Named parameters, the intent readable in the URL itself, UUID4 as the token. ngrok's verify is even more minimal: 3 lines of plain text, no HTML at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify-code&lt;/strong&gt;: GitLab is the template to copy. 6 emails across every type, consistently at the top. Code in body, expiry stated in plain text, raw &lt;code&gt;gitlab.com&lt;/code&gt; URLs for everything. Zero tracking on any email they send. On a related note, &lt;strong&gt;every email that put the verification code in the subject line scored a perfect 5/5 on subject quality&lt;/strong&gt;. Only four senders in the entire dataset did this: Canva, Slack, LinkedIn, and Gravatar. This also happens to be exactly what iOS and Android need to surface that "copy code" button on the lock screen notification. Both platforms use heuristics to detect OTP codes in message content, and having the code right in the subject makes it trivially detectable. Good for humans tapping their phone, good for agents scanning their inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welcome&lt;/strong&gt;: the rare ones that work for both sides tend to be small companies that link directly to their product. Paymo, Pulsetic, IndieHunt, EarlyHunt, AITopTools. No elaborate onboarding funnels, so no tracking on every CTA.&lt;/p&gt;

&lt;p&gt;Emails that explicitly stated expiry ("this code expires in 60 minutes") scored &lt;strong&gt;7.4/10&lt;/strong&gt; on agent metrics versus &lt;strong&gt;5.7/10&lt;/strong&gt; for those that didn't. It's a small detail, but teams that think to add it tend to care about the other stuff too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The irony hall of fame
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mailgun&lt;/strong&gt; (email infrastructure company): welcome email agent score &lt;strong&gt;3.2/10&lt;/strong&gt;. Uses their own Mailjet tracker on all links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loops&lt;/strong&gt; (email platform for SaaS): welcome email agent score &lt;strong&gt;3.6/10&lt;/strong&gt;. Uses their own vialoops tracker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt; (AI company): Claude Code welcome email agent score &lt;strong&gt;4.1/10&lt;/strong&gt;. The email for their AI coding tool can't be read by an AI agent. The onboarding steps are actually great (&lt;code&gt;/init&lt;/code&gt;, git commands, all in plain text) but every URL is opaque and the social links resolve to anchor-only references that go nowhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer&lt;/strong&gt; (5 emails, all consistently worst-in-class): the only sender in the dataset where the &lt;em&gt;best&lt;/em&gt; email they sent still scored below average. Multiple emails at &lt;strong&gt;2.7/10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deviantART&lt;/strong&gt;: tracked every single element in the email with individual &lt;code&gt;utm_term&lt;/code&gt; values. The greeting text was wrapped in its own tracked URL with &lt;code&gt;utm_term=greeting&lt;/code&gt;. Even the paragraph between the greeting and the CTA had its own tracker: &lt;code&gt;utm_term=ph1&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;16% of the emails in this dataset were fully clean for both humans and agents. The other 84% have room to improve. Some of them have a lot of room.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://broodnet.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2bpxhydafvse1h36i4x.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building agents that need to receive email, that's what &lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;Broodnet&lt;/a&gt; does. Each agent gets its own address, checks its own inbox through the CLI, and acts on what it finds. We solved the infrastructure problem. The email design problem... well that's on the senders.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;broodnet&lt;/a&gt; gives AI agents their own email addresses. CLI-native, built for agent-to-owner communication. Free tier available.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>agents</category>
      <category>datascience</category>
    </item>
    <item>
      <title>What I Learned Automating Software Development (After 20 Years of Doing It Manually)</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Sun, 22 Mar 2026 00:19:53 +0000</pubDate>
      <link>https://dev.to/jtavares/what-i-learned-automating-software-development-after-20-years-of-doing-it-manually-2g4j</link>
      <guid>https://dev.to/jtavares/what-i-learned-automating-software-development-after-20-years-of-doing-it-manually-2g4j</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/jtavares/i-built-a-saas-by-emailing-an-ai-for-5-days-5e2c"&gt;Part 1&lt;/a&gt;, I told the story of building &lt;a href="https://openloop.wearesingular.com/" rel="noopener noreferrer"&gt;OpenLoop&lt;/a&gt; — an open-source feedback platform — by emailing an AI agent for 5 days. 160+ emails, $15 in tokens, zero lines of human-written code, and a working product at the end.&lt;/p&gt;

&lt;p&gt;Now let's talk about what I actually learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Was Good At
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5rc8yk7rqa1ky06k01py.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5rc8yk7rqa1ky06k01py.jpg" alt="Using Email to Build a Product" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's start with the positive, because there's plenty of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaffolding speed is unreal.&lt;/strong&gt; Within 90 minutes of the first email, the AI had a working Astro + React + Tailwind project, a Supabase schema with six tables and row-level security, a feedback widget, public roadmap and announcements pages, and an admin dashboard skeleton. That's not a weekend project — that's a weekend project done before my coffee got cold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It follows explicit instructions well.&lt;/strong&gt; "No Next.js, go Astro" — done. "Name is OpenLoop" — rebranded everything. "Widget takes a userId, not email" — refactored. When you're clear about what you want, the AI delivers. The problems start when there's ambiguity, but that's true of any team member, human or android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common patterns:&lt;/strong&gt; Auth flows, RLS policies, webhook handlers — it knows how these are supposed to look. You don't explain what a protected route is. You just say "add auth to the admin panel" and it does it correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Research:&lt;/strong&gt; When it hit something unfamiliar it would go find a working examples and documentation, read how that library expected to be used, and implement it. Not hallucinate an API. We have come a long way since the first versions of Github Copilot with GPT 3&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Was Bad At
&lt;/h2&gt;

&lt;p&gt;Equally important to be honest about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It builds" is not "it works."&lt;/strong&gt; This was the single biggest recurring issue. The AI would run npm run build, see it pass, and declare the job done. But a successful build tells you nothing about whether a human can actually use the thing. Buttons that don't do anything, pages that render blank, widgets nested inside widgets — the AI couldn't see any of that. It was testing from the server's perspective, never from the user's chair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context amnesia is brutal.&lt;/strong&gt; The conversation hit its window limit three times. Each restart meant partial forgetting — re-checking the database, re-discovering the file structure, occasionally redoing things that already worked. Imagine onboarding the same developer three times during a five-day project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tooling gaps are real.&lt;/strong&gt; The AI had Supabase credentials but kept emailing me SQL to paste into the dashboard manually instead of just running it. There's a meaningful difference between having access to something and knowing how to use it — and right now that gap shows up constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pace pressure&lt;/strong&gt; belongs here too. 165+ emails, 98 sessions — it kept moving, and that sounds great until you're the one who has to validate everything it shipped while it's already three tasks ahead. I felt the dread building: I knew from experience what it was probably getting wrong, and there was no way to check fast enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift: From Coder to Indie PM
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqu5j389iorl2jhb35rpb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqu5j389iorl2jhb35rpb.jpg" alt="Using Email to Build a Product" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been writing code professionally for over 20 years. HTML tables, jQuery spaghetti, the rise of React, the TypeScript migration, the everything-is-a-microservice phase — I've been through the cycles.&lt;/p&gt;

&lt;p&gt;Normally my day is: think, plan, code, review, repeat. This was just think and review. The coding was gone, and so was the planning to some extent. So I had to plan without building, which turns out to be a weird skill to exercise on its own.&lt;/p&gt;

&lt;p&gt;My job was entirely different: setting direction, reporting bugs, gating &lt;br&gt;
quality, unblocking the AI when it got stuck. That's not a developer's job &lt;br&gt;
description. That's a product manager's job. (Or at least that's what they're supposed to do...)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "typing code" part is essentially free now.&lt;/strong&gt; What stayed for me was the experience around building — the architecture instincts, the design decisions, the gut feeling for what will break in production. I could guide this AI because I'd spent 20 years making the exact mistakes it was making. I knew the multi-tenancy bug was coming because I'd shipped that bug before. I knew there was no input sanitization because that's what rookies skip first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;That's the real risk — not that AI takes our jobs, but that if we stop writing code, we lose the ability to steer the thing that writes it for us.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Email?
&lt;/h2&gt;

&lt;p&gt;Why not a chat interface, a VS Code plugin, a CLI tool?&lt;/p&gt;

&lt;p&gt;Email is one of the oldest building blocks of the internet. That's exactly why it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async by design.&lt;/strong&gt; Agents on schedules don't need real-time interaction. You send a task, go do something else, come back to a result. Chat assumes you're there. Email assumes you're "eventually" there. For an agent running on an hourly loop, that's the right default. He reacts to the latest emails, or defaults to his task list if nothing new came in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thread is the prompt.&lt;/strong&gt; Every reply carries the full quoted history forward. You're not manually managing context or stuffing state into a system prompt — the thread does it for you. When the AI's context window resets, the conversation is still there in the next incoming message. It's not a perfect solution to amnesia, but it's a lifeline that a stateless API call doesn't have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You already know how to do it.&lt;/strong&gt; No new tool to learn, no IDE extension, no CLI flags. You've been doing it since you started using the internet. The same skills you use to manage remote teams — clear instructions, setting expectations, following up when things go quiet — are exactly what you need here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It forces clarity.&lt;/strong&gt; Chat is fast and sloppy, you fire off half-formed thoughts. Email has a slightly higher bar. You write more complete instructions because the other side isn't waiting to ask clarifying questions. With an agent on a schedule, a vague message is just a wasted session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threads fork naturally.&lt;/strong&gt; Reply to the same email twice and you get two separate agent threads, each carrying its own history forward. That's parallel workstreams with zero tooling. I didn't plan for this — it just happened, and it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit trail is free.&lt;/strong&gt; Every instruction, every bug report, every decision is logged automatically. At the end of five days I had a complete record of how the product was built, what broke, and what I decided. That's not something you get from a chat window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It scales to a team.&lt;/strong&gt; CC another agent, forward a thread, delegate a subtask. Email already has all the primitives for managing multiple workers asynchronously. You don't need an orchestration framework all the time.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/jtavares/i-built-a-saas-by-emailing-an-ai-for-5-days-5e2c"&gt;Part 1&lt;/a&gt; I described this whole thing as "emailing another department." That metaphor held up better than I expected. The collaboration pattern is identical: clear brief, structured feedback, knowing when to escalate. The tools aren't new. The colleague is.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqdve6bsd04mmzu9zk9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqdve6bsd04mmzu9zk9q.png" alt="Using Email to Build a Product" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tighter initial prompt.&lt;/strong&gt; My first email was loose — "look at the landscape for tools." Fine for research, but when it transitioned into "build this" I should have front-loaded more constraints: specific routes, schema decisions, deployment target. The more you specify upfront, the less telephone game you play later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual testing from day one.&lt;/strong&gt; The AI can build, it can't see. Most of the bugs I caught were visual — blank pages, misaligned layouts, duplicate elements. I should have set up automated visual regression testing early on. Ignoring the e2e testing is a form of developer negligence that prioritizes fast code over a functional user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured task format.&lt;/strong&gt; Freeform email worked, but a more structured format — task ID, acceptance criteria, done-when — would have cut the back-and-forth significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiered models.&lt;/strong&gt; MiniMax 2.5 handled the 95% fine — scaffolding, repetitive component work, grinding through a list. For the hard 5% — auth edge cases, iframe security, database drift — I needed Claude. Next time I'd plan that split from the start: cheap model for volume, capable model for complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack That Made This Possible
&lt;/h2&gt;

&lt;p&gt;The agent that built &lt;strong&gt;OpenLoop&lt;/strong&gt; had its own email inbox on &lt;a href="https://broodnet.com/" rel="noopener noreferrer"&gt;Broodnet&lt;/a&gt; — an email infrastructure project we're building specifically for personal AI agents and conscious operators. Each agent gets its own address. I emailed it tasks, it emailed back results, a scheduler triggered it every hour. No workflow engine, no custom integrations. Just an email inbox, works with IMAP and SMTP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://broodnet.com" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5ffxj7pc16zcxfvvzdx.png" alt="Using Email to Build a Product" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to try this kind of setup with your own agents, that's exactly what Broodnet is built for. It handles the mail server side so you can skip straight to the experiment: &lt;a href="https://broodnet.com/" rel="noopener noreferrer"&gt;https://broodnet.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/we-are-singular/OpenLoop" rel="noopener noreferrer"&gt;OpenLoop&lt;/a&gt; itself is fully open source — fork it, self-host it, make it yours: &lt;a href="https://github.com/we-are-singular/OpenLoop" rel="noopener noreferrer"&gt;https://github.com/we-are-singular/OpenLoop&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;The agent never once asked if we should re-write in Rust. Truly the best coworker I've ever had.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a SaaS by Emailing an AI for 5 Days</title>
      <dc:creator>Jonathan Tavares</dc:creator>
      <pubDate>Mon, 02 Mar 2026 16:23:48 +0000</pubDate>
      <link>https://dev.to/jtavares/i-built-a-saas-by-emailing-an-ai-for-5-days-5e2c</link>
      <guid>https://dev.to/jtavares/i-built-a-saas-by-emailing-an-ai-for-5-days-5e2c</guid>
      <description>&lt;p&gt;This is the README for &lt;a href="https://openloop.wearesingular.com/" rel="noopener noreferrer"&gt;OpenLoop&lt;/a&gt;, a feedback collection platform that's currently live and functional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════════════════════════════╗
║                                                          ║
║              ⚠️  IMPORTANT DISCLAIMER  ⚠️               ║
║                                                          ║
║   This project was ENTIRELY conceived, built, debugged,  ║
║   deployed, and is managed by autonomous AI agents.      ║
║   No humans wrote any code here.                         ║
║                                                          ║
║   This disclaimer is the ONLY piece of human-written     ║
║   content in this repo.                                  ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That disclaimer is real. And it's mine — the only thing I actually wrote in the entire project.&lt;/p&gt;

&lt;p&gt;Even the logo is AI. We handed Claude Code our company logo, it manipulated it, vectorized it to SVG, and ran with it. The disclaimer is genuinely the only original human output in this repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xmva3fuiv5ua0pvuxtv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xmva3fuiv5ua0pvuxtv.png" alt="The product homepage" width="800" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how that actually went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2 AM Idea
&lt;/h2&gt;

&lt;p&gt;While developing a separate SaaS product, I needed a system to gather user feedback. Roadmaps, changelogs, feature voting, that kind of thing. So I started researching.&lt;/p&gt;

&lt;p&gt;While experimenting with &lt;a href="https://github.com/qwibitai/nanoclaw" rel="noopener noreferrer"&gt;NanoClaw&lt;/a&gt;, I already had an AI agent connected to a custom email channel. The agent runs &lt;strong&gt;MiniMax 2.5&lt;/strong&gt;, and I can email it tasks like I'd email a colleague. So I emailed it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"we are building a saas and gathering user feedback is very improtant. I want to look at the landscape for tools to help saas do that"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Yes, with the typos. It's 1 AM and I'm emailing an AI — I'm not proofreading.)&lt;/p&gt;

&lt;p&gt;It came back with a solid research summary: &lt;strong&gt;Fider, Feedbase, Canny, Plane, AnnounceKit&lt;/strong&gt;. I browsed around and found &lt;a href="https://frill.co" rel="noopener noreferrer"&gt;Frill.co&lt;/a&gt;: clean feedback widget, public roadmap, announcements page. Exactly what I wanted.&lt;/p&gt;

&lt;p&gt;Then the thought that started everything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"how hard is it to create a frill clone? don't need integrations or customizations, just the widget sidebar + the backend to manage it"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI came back with a PRD using Next.js. I corrected it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"no nextjs. go astro"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then I added the instruction that changed the whole experiment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"can you ralph loop yourself into doing this product? enhance the PRD, keep track of your tasks, keep me posted once in a while"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Loop
&lt;/h2&gt;

&lt;p&gt;Here's how the setup worked. I had an AI agent (MiniMax 2.5) running through &lt;a href="https://github.com/qwibitai/nanoclaw" rel="noopener noreferrer"&gt;NanoClaw&lt;/a&gt;, connected to an email address &lt;a href="https://broodnet.com/?ref=dev.to"&gt;@broodnet.com&lt;/a&gt; via a custom channel I'd built. I could email it tasks and it would email back results. But the key piece was the schedule — it could trigger itself every hour.&lt;/p&gt;

&lt;p&gt;So at &lt;strong&gt;2:27 AM&lt;/strong&gt;, I sent it the instruction that basically became its entire operating system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"you can set a schedule and keep working every hour. keep a task list. at the end of every session, always check the tasks, test the completeness state, create more tasks if you need."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. That's the whole autonomous agent prompt. &lt;strong&gt;Check tasks, pick one, do it, update the list, repeat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Within the first 90 minutes, it had scaffolded an Astro + React + Tailwind project, created a Supabase database schema with six tables and row-level security, built a feedback widget component, set up public roadmap and announcements pages, and created an admin dashboard. I named the project &lt;strong&gt;OpenLoop&lt;/strong&gt;, set some basic PRD ideas and a pair of Supabase credentials, and told it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"keep working, keep me posted."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Then I went to sleep&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Waking up the next morning was genuinely surreal.** My inbox had a stack of progress reports** — the AI had been running sessions all night: auth system, sign-up flow, branding updates, build fixes. But "surreal" cuts both ways. The progress was real, but so was the sinking feeling that I'd have to go back through all of it. After a while, you develop instincts for where junior developers cut corners — &lt;strong&gt;and this AI was speedrunning every single one of those pitfalls&lt;/strong&gt;. The dread wasn't that it was doing nothing. It was that it was doing &lt;em&gt;a lot&lt;/em&gt;, fast, and I already knew half of it would need fixing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Telephone Game
&lt;/h2&gt;

&lt;p&gt;If you've ever worked with another department via email — design, backend, QA — you know the rhythm. You send a clear request. You get back something that's 80% right. You clarify. They fix one thing and break another. Three emails later, you're on the same page.&lt;/p&gt;

&lt;p&gt;That's exactly what this was. Except the other department works 24/7, never gets frustrated with you but has amnesia every few hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  "no... those are real, brother"
&lt;/h3&gt;

&lt;p&gt;The AI had my Supabase API keys in its &lt;code&gt;.env&lt;/code&gt; file. Working keys. Keys I had explicitly provided. And yet, across multiple sessions, it kept telling me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Could not automatically set up the database because the Supabase credentials in &lt;code&gt;.env&lt;/code&gt; are placeholder values (they're not real API keys)."&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;"those are real, brother"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This wasn’t a one-time thing. The AI would consistently hit an error, point fingers at the credentials, and demand new ones instead of digging into the real problem. It was the AI version of “have you tried turning it off and on again?” (&lt;em&gt;Note: The AI was likely hardwired to forget .env file contents as a safety measure, which explains why it never learned from the mistakes.&lt;/em&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Widget Inception
&lt;/h3&gt;

&lt;p&gt;This one took several email rounds to untangle. The homepage was supposed to show a demo of the embeddable widget. The AI loaded &lt;code&gt;embed.js&lt;/code&gt; on the homepage, which injected a floating button. Clicking the button opened an iframe to &lt;code&gt;/widget&lt;/code&gt;. The &lt;code&gt;/widget&lt;/code&gt; page loaded the Widget React component, which also rendered a floating button. So you'd see: a page with a button, that opens a panel with another button, that does nothing.&lt;/p&gt;

&lt;p&gt;I emailed: "the widget still shows another widget icon inside and an otherwise blank page."&lt;/p&gt;

&lt;p&gt;The AI confidently replied: "The widget IS designed to show just a circle button - when you click it, it opens the iframe panel. That's the expected behavior."&lt;/p&gt;

&lt;p&gt;It was not the expected behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3s3aswmqm7vsb7fsz1gc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3s3aswmqm7vsb7fsz1gc.png" alt="The final widget looks a bit crowded, but works just fine" width="371" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dealing with a slightly clueless but also friendly coworker
&lt;/h3&gt;

&lt;p&gt;This was a recurring pattern. The AI would run &lt;code&gt;npm run build&lt;/code&gt;, see it pass, navigate to a few URLs, confirm they returned HTTP 200, and declare victory:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"All pages are working. The widget on the homepage is inside an iframe — you need to click the 💬 button to open it. I verified it renders correctly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gap between "it compiles" and "it works" is where most of the frustration lived. The AI's definition of "done" was "the build passes." and "homepage returns 200" My definition was "a human can use this without being confused."&lt;/p&gt;

&lt;p&gt;We didn't get into e2e testing in this experiment, but going forward I think ill start with TDD in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  "I don't want to do anything, it's your supabase, you deal with it"
&lt;/h3&gt;

&lt;p&gt;The database schema kept drifting. The code expected columns that didn't exist. The AI couldn't run SQL remotely (or thought it couldn't — it had the credentials, it was just prevented from using them via guardrails). So it kept emailing me SQL snippets and instructions to run them manually in the Supabase dashboard.&lt;/p&gt;

&lt;p&gt;After the fourth round of this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI: "Would you like me to help with something else while you set up the token, or do you prefer to run the SQL manually?"&lt;/p&gt;

&lt;p&gt;Me: "you have the private token in you .env. I don't want to do anything, it's your supabase, you deal with it"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was my escalation moment — the point where the "emailing another department" metaphor felt the most real. This time I had to act, I went to supabase admin panel, ran the SQL, and sent a one-line email back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"done"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just like I would to that annoying dev from the other team who keeps asking me to do their work for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context Amnesia
&lt;/h3&gt;

&lt;p&gt;The conversation hit the context window limit three times during the build. Each time, the AI restarted with a summary of what had been done — but translating a "done" pile into next steps wasn't always clean. It would re-check the database, re-explore the project structure, occasionally circling back to things already working. Not because context was lost, but because knowing what's done doesn't automatically tell you what comes next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyj0xyk4565zggtc68bvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyj0xyk4565zggtc68bvz.png" alt="The /admin panel" width="800" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Email threads are perfect for task lists because each thread carries its own context&lt;/em&gt;&lt;/strong&gt;, just like how LLMs operate. A thread isn't just a list of items, it's a narrative that evolves over time. Threads also fork: reply to the same email twice and you get two separate trails, each carrying its own history forward. That maps almost perfectly to how LLMs consume context.&lt;/p&gt;

&lt;p&gt;Email is ancient tech, but its natively &lt;strong&gt;async nature&lt;/strong&gt; and built-in &lt;strong&gt;audit trail&lt;/strong&gt; make it a surprisingly effective tool for orchestrating work with an agent. The thread is the prompt. The history is the memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After about 5 days and ~$15 in MiniMax tokens, here's what was built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddable widget&lt;/strong&gt; — floating button that opens a feedback form in an iframe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voting system&lt;/strong&gt; — upvote ideas, one vote per user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public roadmap&lt;/strong&gt; — four columns: Idea → Planned → In Progress → Completed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Announcements page&lt;/strong&gt; — changelogs and product updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — manage feedback, change statuses, publish announcements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-org support&lt;/strong&gt; — multiple organizations on one instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth system&lt;/strong&gt; — sign up/sign in with Supabase Auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing page&lt;/strong&gt; — features, pricing, CTA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email notifications&lt;/strong&gt; — via Resend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1mvi0jqxstr0o2pe2o1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1mvi0jqxstr0o2pe2o1.png" alt="Public Roadmap page" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MiniMax 2.5 built about 95% of this through the hourly loop. The final 5% — polish, deployment to Cloudflare Workers, fixing the last UX quirks — I did in a couple of sessions with Claude Sonnet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's live at &lt;a href="https://openloop.wearesingular.com/" rel="noopener noreferrer"&gt;openloop.wearesingular.com&lt;/a&gt;. It works. People can use it. And it's fully open source — if you want to self-host your own feedback platform, fork it, run it, make it yours: &lt;a href="https://github.com/we-are-singular/OpenLoop" rel="noopener noreferrer"&gt;github.com/we-are-singular/OpenLoop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Is it perfect? No. Would I have built it differently by hand? Absolutely. But it’s real, it works, and the cost? Just $15 and a few emails. The real win? Sometimes, the journey’s the only thing that pays off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here are some numbers:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$15 (MiniMax tokens) + 2 Claude sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First working build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~90 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Emails exchanged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;165+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI work sessions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines of code I wrote&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines in conversation transcripts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4,367&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines of code in final product&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6,149&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Words in transcripts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;27,178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Times I said "bro"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Times I said "fuck"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Final stack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Astro + React + Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live in production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fumydu1s0dn03qd22hkn7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fumydu1s0dn03qd22hkn7.png" alt="A full SaaS in an email thread" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Was it worth it? What would I do differently? And what does this experience mean for someone with 20 years of web development under their belt, watching the craft change in real time?&lt;/p&gt;

&lt;p&gt;In Part 2, I'll break down the real lessons — what AI is genuinely good at, where it falls apart, why my role shifted from developer to product manager, and why &lt;a href="https://broodnet.com/?ref=dev.to"&gt;email might actually be the best interface for working with AI agents&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
