While I work to complete my new book “AI-Driven API Design”, I’m keeping my posts here sparse (see below) and light (see my “Approved by Management” comic series). I also apologize in advance for the “instructional tone” this piece has as my brain is in that head space while I work though the new book. I’ll return to my story-telling posts later this year once the book is off my plate. -- mamund Over the last year or so I’ve been experimenting with a simple model for empowering autonomous agents in constrained environments. I’ve dubbed this work “Goal-Resolution through Affordance-Informed Logic” or GRAIL since one of the key elements in the model is defining and referencing affordances (links and forms) for clients to call. NOTE : I’ve discussed my GRAIL work here on substack in the past. You can go to my archives page and search for “GRAIL” and see several articles. Feel free to refer to the previous articles at any time. Recently I created some experiments exploring how the system holds up when the service’s preconditions are presented to the client in random order. Does this model hold up even when the possible “next steps” are presented in random order? The GRAIL Algorithm As a quick refresher, the GRAIL algorithm is very small and, so far, very effective. The code for a fully operational GRAIL client is less than 50 lines of NodeJS. That includes console.log statements peppered through the instrumented client. The algorithm itself is straightforward: 1. Start with a goal 2. Attempt the action associated with that goal if SUCCESS then EXIT if BLOCKED by an unmet precondition, follow an available action that can satisfy the unmet condition then go to step 2 Continue until the original goal succeeds or there is no available path forward. The trick is to start at the end. Try the end goal first. This algorithm also depends on services knowing their own preconditions (you must be logged in first, you must have a non-zero balance, etc.) and exposing those preconditions to the client when they are unmet. Of course, this is all recursive so attempting to execute on an unmet precondition might also return a BLOCKED message with one or more unmet preconditions for that precondition; and so forth. Visually, it looks like this: At the heart of all this is to a registry of affordances (actions) arranged as pre-conditions for a particular goal (onboardCustomer, computeNetPresentValue, produceOverdueAccountsReport, and so forth). The registry is, essentially, the environment in which the agent exists and interacts with affordances. Let’s Randomize it A set of actions lives in an environment and that environment is expressed in a registry.json file. Each entry lists all the details needed to successfully execute the action such as name, associated function, inputs, expected outputs, side effects, and (if any) preconditions. The client is built to understand this standardized representation and uses the affordance collection to complete its assigned goal. In some cases, the affordance has quite a few preconditions that need to be met before the client can execute the selected action. For example, the registry entry below shows an affordance with eight preconditions. And, as we’ve already stated, it may be true that any one of these eight preconditions have their own list of additional preconditions (and so forth). The assertion I set out to test is whether the GRAIL system requires execution of preconditions in a specific order (e.g. in listed order). Since my current examples are all run as CLI utilities (there is no MCP/LLM needed), I needed to add a bit of randomization to the unmet precondition routine. This would approximate the possibility that a client app might not execute the preconditions in a fixed (and repeatable) order. All it took for that was to add a single line of code that randomly selected one of the unmet preconditions in the runtime registry: if (unmetPreconditions.length > 0) { const pre = unmetPreconditions[ Math.floor(Math.random() * unmetPreconditions.length) ]; Now, when I run the test, each trace has the potential to be slightly different. The questions is: Does the client consistently complete the goal, no matter the execution order of the preconditions? Tracing in GRAIL The GRAIL test harness has the ability to produce a trace file that shows the path of the client through the provided environment. With my randomizer in place, I ran several traces and happily discovered that first, each traced path is different and second, all traced runs reached the goal successfully. Below are screenshots of two example traces. Note the step-by-step order in the numerals next to each navigation line in the diagram. In this first example (below), you can see that the first precondition executed (after attempting the goal directly) is startCustomerOnboarding. Then there’s a series of back-and-forth actions to resolve the remaining preconditions before returning to execute the associated goal action. However, in the second run below (see below), the client followed a different path in attempts to resolve most of the minor details (starting with verifyCustomerPhonenumber before actually finding the startCustomerOnboarding action. An interesting side note that I did not have time to explore is that, while each trace shows a unique navigation order, both traces took the same number of steps (16) to complete. In fact, all the traces I ran took 16 steps. This leads me to assume that, while the navigation path may vary, the differences in path order do not impose any additional execution cost. There is no “cheaper” path in these GRAIL models. Different Routes, Same Destination In most workflow type systems, we’re lead into authoring a fixed series of steps in order to reach a goal. for example, an approximate rendering of my above onboarding example looks like this in Arazzo (see specs here). And it is important to note the the example below is a fixed set of steps in order. arazzo: 1.0.1 info: title: Customer Onboarding Workflow version: 1.0.0 sourceDescriptions: - name: customerApi url: ./openapi.yaml type: openapi workflows: - workflowId: onboardCustomer summary: Satisfy customer onboarding requirements and onboard the customer steps: - stepId: onboardingStarted operationId: startOnboarding - stepId: customerProfileCollected operationId: collectCustomerProfile - stepId: customerEmailSet operationId: setCustomerEmail - stepId: customerEmailVerified operationId: verifyCustomerEmail - stepId: customerPhoneNumberSet operationId: setCustomerPhoneNumber - stepId: customerPhoneNumberVerified operationId: verifyCustomerPhoneNumber - stepId: customerAddressSet operationId: setCustomerAddress - stepId: customerTermsAccepted operationId: acceptCustomerTerms - stepId: onboardCustomer operationId: onboardCustomer successCriteria: - condition: $statusCode == 200 I’m even seeing people working on creating agents that do this kind of planning before they attempt to reach the goal. I think this is not needed and makes the work of designers and engineers harder than it needs to be. The GRAIL model is built in a way that agents are free to take more than one particular path through the environment (as defined by the registry.json file) and still meet the intended goal. And they can do that without any hints or set up from a human. They just need the end goal and access to the environment. Conclusion GRAIL shows us we can create systems where we don’t need to know exactly what that path will be before starting a run. And we don’t need to guarantee the same path is traced each time the agent attempts to reach the goal. Essential guidance for GRAIL authors would be: Define the environment, not the path. If this kind of thing is interesting to you, feel free to check out the public GRAIL repo where some other experiments will be appearing in the near future.
Subscribe now
Subscribe now
Subscribe now
For regular readers, you may have noticed that I’ve been writing less here on Substack lately. That’s because I’ve been deep into writing my latest O’Reilly book, “AI-Driven API Design”. The book grew out of the workshops of the same name that I’ve been presenting over the past year. Now that the manuscript is taking shape, I’m taking the newly updated workshop on the road again. As AI becomes part of everyday software development, both this workshop and my forthcoming O’Reilly book explore a simple question: What does good API design look like in the age of AI? Subscribe now The fundamentals haven’t changed. Every successful API still begins with understanding the problem domain, identifying the people and systems involved, capturing intended behavior, and designing interfaces that are clear, reliable, and easy to evolve over time. What has changed is the design process itself and the tools at our disposal. AI can help us discover, model, document, implement, and validate designs in ways that were impractical or too costly just a few years ago. This is an opportunity to preserve quality while reducing the effort required to get from idea to install. Those are the ideas I’ve been exploring in my forthcoming O’Reilly book, “AI-Driven API Design”, and they’re the foundation for this workshop tour. In the workshop, we’ll work through a complete AI-assisted design process together. Starting with domain discovery and API Stories, we’ll move through ALPS, OpenAPI, behavioral testing, and traceability to see how intended behavior can be preserved from the first design conversation through running software. Along the way, we’ll explore how each design artifact contributes to APIs that humans and AI agents can understand, trust, and use with confidence. If you’ll be at one of these public events, I’d love to see you there. Follow the links for registration. September 1 — API World (Santa Clara, CA) September 17 — O’Reilly Live (Online) September 23 — We Are Developers World Congress North America (San Jose, CA) October 1 — API Conference New York (New York, NY) Discount: Join_APINY26 November 20 — API Conference Berlin (Berlin, Germany) The conversations that emerge during this tour will also help shape the final stages of the book. If you’ll be attending one of these events, I hope you’ll stop by and share your own perspective on what good API design looks like in the age of AI. Finally, if your organization or event is interested in hosting this workshop, or if there’s a city where you’d like to see it offered, let me know. I’d enjoy bringing the conversation to your community. Subscribe now
Subscribe now
For most of my software career I have focused on APIs, distributed systems, hypermedia, and software architecture. My writing, speaking, tool-making, and advising have all centered around improving the design, implementation, and evolution of running systems. I gravitated toward APIs because it was there, most clearly, that design decisions, both good and bad, had a direct and visible impact on the value and quality of software in the real world. It was there that I learned, and eventually taught others, how early choices shape later options or, as I often say, how to get from idea to install. Looking back over that journey, there are three ideas that connect nearly everything I’ve done over the last fifty years. Software development is a sequence of translations. Software architecture is about coordinating translations. Software engineering reduces manual translation while preserving behavioral intent. Let’s take each of these in turn... Subscribe now 1. Software development is a sequence of translations. Every software project begins with a story: an idea of intended behavior. Typically, the phrases “It would be great if we could ...” or “Our customers really want ...” and so forth. There is a set of actions that need to be translated from idea form into something more tangible. We give these translations different names such as requirements, user stories, data models, API descriptions, source code, test scripts, but they’re all doing the same thing. Each is another attempt to preserve behavioral intent while expressing it in a form suitable for the next participant in the process. This is often referred to as the software development life cycle (SDLC), Of course, every translation introduces opportunities for misunderstanding, drift, omission, and accidental complexity. And usually these translations are chained together in a sequence. This means the translation of the idea into a user story is one step and then translation of the user story into an API description document (e.g. Open API) is another translation. And that second translation depends on the quality and accuracy of the first translation (idea to story). Translations upon translations is the norm. Every translation inherits the strengths and the weaknesses of each translation that preceded it. The challenge isn’t simply writing software. It’s preserving behavioral intent across all the translations in the software development lifecycle. Designers must think not only about each translation, but also about the dependencies that form between translations. As the cost of generating source code continues to fall, the important question is “Is this code a faithful translation of the behavioral intent that came before it?” And what kind of evidence do you have that each translation along the way continues to preserve and protect the intended behaviors? Validating behavior preservation requires tooling. Translation is a skill. And that raises the question: if software development is a sequence of translations, what exactly is the role of software architecture? 2. Software architecture is really about coordination. If software development is a sequence of translations, software architecture determines where, when, and how those translations and the resulting systems coordinate. And coordination sits at the heart of building effective, available, scalable, and efficient systems (EASE). Looking back, I’ve come to see the history of software architecture as the history of reducing and reallocating coordination. Whether between data models and object models, services and clients, or people and teams, architecture has always been about deciding where coordination belongs. Amundsen’s Maxim is a haiku on the topic of systems coordination. Early systems relied heavily on meetings, requirements documents, and manual coordination. Object-oriented methods and UML attempted to coordinate increasingly complex systems through shared models. Service-oriented architectures and APIs shifted coordination into well-defined interfaces. Amazon’s “API Mandate” pushed even further, insisting that teams coordinate through explicit contracts rather than hallway conversations. After years of thinking about APIs and distributed systems, I’ve come to this definition: Software architecture is the art of determining where, when, and how coordination occurs so that intended behavior can be achieved while minimizing accidental coordination. Viewed this way, architectural styles are simply different strategies for managing coordination. I have spent my time on architecture models that favor extreme late-binding (via hypermedia affordances), reduced central control (via composable systems), and runtime adaptation. They differ less in what they build than in how they allocate coordination. Which brings us to the third idea. Nearly every major advance in our field has reduced the amount of manual translation and coordination required to preserve intended behavior. 3. Software engineering reduces manual translation while preserving behavioral intent. When I started in this field, I spent hours hand-writing COBOL programs on yellow legal pads, checking logic with truth tables and sequence charts before ever touching a computer. Then I’d wait for an open terminal to type everything in and submit the job to a batch queue. Hours later, I’d finally get the results—hoping for a few pages of successful output instead of a thick stack of compiler errors. Looking back, what strikes me most is how much of software development consisted of manual translation. Over the decades we’ve steadily reduced the manual effort required to translate ideas into running systems. Compilers eliminated one translation. High-level languages eliminated another. Libraries, frameworks, and cloud platforms reduced still more. AI continues that same trajectory. Together, these advances reveal a decades-long trend: Every major advance in software engineering has reduced manual translation without losing sight of behavioral intent. The work of translation is the work of abstraction; of converting thought into action. For decades we treated source code as the definitive description of a system. Increasingly, that role belongs to behavioral intent. Source code has become another translation rather than the destination. As manual translation disappears, evidence becomes more important than ever. The question is no longer who wrote the code. The question is whether the implementation faithfully preserves the intended behavior. Languages evolve. Platforms come and go. Even source code is becoming less central than it once was. But software engineering has always been, and will continue to be, the discipline of preserving behavioral intent across successive translations. The real question is whether behavioral intent survives the translation. Closing While I am best known for my work on web APIs, I have always thought of it as part of a broader effort to improve the way we design, build, and evolve software. REST, hypermedia, CSP, GRAIL, TRAM, and now AI coaching are simply different explorations of the same underlying question: How do we preserve intended behavior while requiring less manual translation and less unnecessary coordination? That question has shaped my work for almost than fifty years. I don’t expect it to stop now. Subscribe now
Subscribe now
Subscribe now
Subscribe now
Subscribe now
Now that summer has arrived in the northern climes, I’ve decided to lighten up on the long-form pieces for a bit and share some comic panels I’ve been playing with for a while. I hope everyone enjoys the summer months and gets a chuckle from my new series, Approved by Management. Let’s see where this leads. And, if you’re so inclined, feel free to drop me a note with feedback or suggestions on where the series might go next. Subscribe now
The ancient Ship of Theseus poses a deceptively simple question. If every plank in a ship is eventually replaced, does it remain the same ship? Software teams encounter a practical version of this question every day. Databases are replaced. Frameworks are upgraded. Services are rewritten. Entire platforms are rebuilt piece by piece. At some point, every part of the implementation may have changed. Philosophers may debate identity. Engineers have a different concern: how do we know the system still behaves as expected? Subscribe now Preventing drift Much of software engineering is the management of change. New features are added. Frameworks are upgraded. Databases are replaced. Monolithic applications are decomposed into services. Entire platforms are rewritten. Through all of these transitions, one concern remains constant: preventing behavior drift while supporting internal modernization. Users rarely care whether an application runs on a monolith or a collection of microservices. They care whether it still does what they expect. A familiar challenge For much of my career, I have worked on systems undergoing exactly these kinds of changes. During the industry’s shift from monolithic applications to distributed service architectures, a significant part of the work involved confirming that externally visible behavior remained stable while the implementation underneath was transformed. To address this problem, teams often built custom validation tools. Some were simple scripts. Others evolved into more sophisticated test harnesses. Their purpose was always the same: capture expected interface behavior and verify that the new implementation continued to honor it. Today, AI-assisted development has changed the scale of this challenge. A team can regenerate substantial portions of an application in hours. Entire subsystems can be refactored, reorganized, or reimplemented with a handful of prompts. In some environments, generated code may change every day. Variability is increasing As implementation variability increases, so does the risk of unintended behavioral change. That makes durable descriptions of expected behavior increasingly important. Consider statements such as: A task can be created. A completed task cannot be completed again. Archived tasks do not appear in active task lists. Invalid requests return meaningful error responses. These statements describe observable outcomes rather than implementation details. They remain meaningful regardless of programming language, framework, deployment model, or architectural style. They describe behavior. More importantly, they describe intent. What remains stable For decades, source code served as one of the primary artifacts of trust in software systems. We reviewed implementations, inspected architectures, and examined changes to understand whether a system continued to behave as intended. Generated software changes that relationship. Multiple implementations can satisfy the same requirements. A feature may be regenerated several times while preserving the same externally visible results. The source code remains important, but it no longer provides the same durable assurance it once did. The implementation may evolve faster than humans can realistically inspect it. Trust increasingly depends on something more stable than the implementation itself. Observable behavior survives implementation change more effectively than source code. Formalizing the practice Over the years I spent helping people with their IT transformations, I found myself accumulating a collection of tools, scripts, and testing approaches designed to capture and verify behavior across changing implementations. Eventually, maintaining those techniques as isolated solutions became less practical. When the need to confirm behavioral stability surfaced yet again recently, I decided to consolidate years of custom validation tools into a single framework. TRAM emerged from that effort. TRAM models observable API behavior as executable assertions that produce evidence. Instead of focusing on how a system is built, it focuses on what an observer should be able to verify at the interface. In TRAM, each assertion becomes a small experiment that produces evidence about how the interface behaves. And these same assertions can be applied to both the pre- and post-transform versions of the system. The implementation may change, but the behavior should not. The goal is to gather evidence that expected behavior has not drifted as the implementation evolves. That perspective becomes increasingly valuable in an environment where software can be regenerated faster than humans can realistically inspect every line of code. Managing change As implementation variability continues to increase, the ability to capture, document, and verify expected behavior becomes more important. Durable behavioral assertions provide a stable reference point. They preserve intent across rewrites, migrations, upgrades, and generated implementations. We learned the value of this approach during earlier waves of modernization. The age of generated software makes it harder to ignore. And relying on behavioral assertions may turn out to become one of the primary ways we establish trust at scale in AI-generated systems. Subscribe now
In a previous life, I was a full-time musician and composer. I spent time every day in practice. Practicing my instrument. Sharpening my ear for harmonies and melodies. Honing my ability to convert what was in my head into notes on a page that others could interpret and perform. Those years taught me something that feels increasingly important in the age of generative AI: repetition matters. Subscribe now I practiced scales not because they were musically interesting, but because of the skills they developed. At the time, scales felt annoyingly simple. Repetitive. Mechanical. Detached from any “real” creativity I was trying to achieve. But over time, that changed. My fingers stopped fumbling. My ear began hearing relationships automatically. Harmonies seemed to create themselves. My musical ideas moved more fluidly from imagination into sound. The practice was reshaping how I thought about sound and rhythm. It increased my ability to articulate what was in my head. I gained new capabilities. The Struggle Is the Craft Often, while practicing, I ran into roadblocks. A difficult melodic turn. A harmonic progression I could hear internally but could not yet execute cleanly. A passage that exposed the gap between what I wanted to express and what I was currently capable of performing. But those moments did not discourage me. They motivated me. Practice taught me not to fear mistakes. It taught me to seek out the edge of my ability; that boundary between facility and struggle where growth actually happens. Over time, I began to understand that the struggle itself was not separate from the craft. The struggle was the craft. That same feeling followed me into software development. Long debugging sessions. Architectural dead ends. Systems that resisted easy answers. And I experience it now while writing; pushing through incomplete ideas, awkward drafts, and concepts that refuse to settle cleanly on the page. In each case, the important work happens when I reach the edge of my abilities. I discovered that creative struggle is evidence that I am stretching, growing, learning. The Cost of Speed Most of the public discussion around AI today centers on speed and efficiency; the ability to increase output without increasing the size of your team. What often goes unspoken is how that efficiency is achieved. In many cases, it comes from removing creative struggle. Staying well within the comfort zone. Avoiding the difficult edge where existing skills are not quite enough. Reducing the need to wrestle with ambiguity, failure, revision, and persistence. But that difficulty boundary is precisely where growth traditionally occurs. That is where musicians develop technique. Where programmers sharpen judgment. Where writers learn clarity. Where architects learn restraint. Where expertise forms. Much of today’s generative AI workflow is designed to export difficulty. The LLM tools flatten out the uncertainty, hide the iteration, the rough drafting, the dead ends, and the partial failures that humans work through themselves in order to gain mastery. Without practice, dexterity wanes. The immediate result is, as so often noted, increased productivity. The long-term risk is reduced capability, atrophied creativity, and weakened skills. Because when difficulty disappears, practice disappears with it. And when practice disappears, so does much of the mechanism through which people improve their skills, sharpen their thinking, and develop durable expertise. Without practice, dexterity wanes. Careful What You Wish For If we are not careful, we could end up creating environments, at work and at home, where people gradually lose their tolerance for creative struggle. Where we assume that valuable work should never feel difficult. That uncertainty is a defect. That friction should always be removed. A world where we get used to giving up early because the system is always ready to complete the thought for us. But that boundary line, the one between competence and challenge, is where growth has always occurred. Instead of building systems that remove struggle, we should be building tools that support growth, demand engagement, and encourage people to remain active participants in the work. We need tools that push us beyond our current capabilities. We do not need more systems that have already looked up all the answers. We need tools that push us beyond our current capabilities. Tools that help us sharpen judgment, extend creativity, and strengthen persistence. Tools that keep us practicing our scales, exploring the harmonies and melodies of everyday life while learning to orchestrate our own future. We need systems where difficulty is not feared, but embraced. We need tools that help us practice. That’s an AI future I can get excited about. Subscribe now
In a future where digital data passes through society as easily as blood through the body, a small groups of devotees isolate themselves in dim, soundproof rooms. There, away from the endless flow of generated output, they engage in a long-lost meditative art. The practitioners claim the practice sharpens perception, transports devotees to new lands, and occasionally yields moments of unusual clarity. But it requires persistence, discipline, and sustained attention, qualities increasingly rare in the digital age. The almost monastic order preserves the practice carefully, even as the broader culture dismisses them as rude, reclusive, even reactionary. What is this rebellious act? They read books. Subscribe now
Two recent statements, one from Nature Reviews Bioengineering and another from the Modern Language Association, arrive at the same conclusion from different directions: Writing is part of thinking and assessing writing is part of teaching. The Nature editorial makes the cognitive case. Writing forces structure onto ideas. It turns scattered research into a narrative, and in doing so, reveals what the work actually means. If that’s true, then outsourcing writing of scientific works to an LLM is not just a productivity play. It is also a shift that threatens to weaken academic work. Subscribe now The MLA statement makes the institutional case. Assessment is a human act of communication. It depends on context, judgment, and experience. Automating it risks hollowing out the role of the educator and redefining learning as something that can be measured mechanically, without being understood. Both land in the same place. While AI can assist, it should not replace. This lines up with an emerging broader pattern as institutions deal with the rise of generative AI. The real risk is not just hallucination or misplaced confidence. It is the steady removal of the steps where people develop skill. Things like drafting, struggling, revising, interpreting, etc. The parts that feel slow, that provide friction, are often the parts where the most important work is done. This is where an AI coaching model, like the one I’ve been exploring, fits in. When used properly, AI can support the creative thinking and writing process: prompting reflection, offering alternatives, helping organize ideas, etc. Used poorly, generative AI short-circuits that creative struggle process and delivers a shallow answer without the thinking that is needed behind it. Generative AI tools can be used to, as Doug Engelbart said, help people “get better and getting better.” Academia, at least in these cases, is leaning into that approach. That’s a good sign. Subscribe now
The erosion of persistence
Following links helps clients move. Understanding forms helps clients choose.