Recent articles

Stochastic Traversal with Deterministic Results
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.
Mike Amundsen1 week ago

Subscribe now
Mike Amundsen2 weeks ago

Subscribe now
Mike Amundsen3 weeks ago

Mike Amundsen4 weeks ago

Subscribe now
Mike Amundsen1 month ago

AI-Driven API Design goes on the road
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
Mike Amundsen1 month ago

Subscribe now
Mike Amundsen1 month ago

Three Things I’ve Learned About Software
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
Mike Amundsen1 month ago

Subscribe now
Mike Amundsen1 month ago


