<?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: rosidotidev</title>
    <description>The latest articles on DEV Community by rosidotidev (@rosidotidev).</description>
    <link>https://dev.to/rosidotidev</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%2F597573%2F7196bb7a-1d95-468f-8afc-a12a54ec9ceb.jpeg</url>
      <title>DEV Community: rosidotidev</title>
      <link>https://dev.to/rosidotidev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rosidotidev"/>
    <language>en</language>
    <item>
      <title>BMAD Method (Part 1): Spec-Driven Development Where the Human Stays in Control</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:17:58 +0000</pubDate>
      <link>https://dev.to/rosidotidev/bmad-method-part-1-spec-driven-development-where-the-human-stays-in-control-41og</link>
      <guid>https://dev.to/rosidotidev/bmad-method-part-1-spec-driven-development-where-the-human-stays-in-control-41og</guid>
      <description>&lt;p&gt;As a software architect and senior software engineer, I keep watching the same change happen in the way we build software. In the first series I wrote about GitHub Spec Kit, and I stayed on one idea that I still think matters most: spec-driven development (SDD). Write the intent first, make it explicit, let the agent build against it, then check the result against what was written.&lt;/p&gt;

&lt;p&gt;That idea does not depend on a framework. Spec Kit is one way to apply it, BMAD Method is another, and a markdown file in your repo is a third. In this series I look at BMAD Method, and in this first article I stay on the concepts and the commands. I do not show a finished result yet.&lt;/p&gt;

&lt;p&gt;My angle is the same as before: not how fast an agent writes code, but who decides what gets built and who answers for what is produced. I think this is the question the industry is starting to ask. Anthropic has published a Claude Academy course, &lt;a href="https://academy.claude.com/courses/ai-native-sdlc-playbook" rel="noopener noreferrer"&gt;The AI-native SDLC playbook&lt;/a&gt;, which asks teams to capture the intent in a version-controlled file and to keep a person in charge at the approval gates. BMAD has been moving in that direction for a while. This is a personal view, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Spec-Driven Development?
&lt;/h2&gt;

&lt;p&gt;Let's start from the problem. Many developers have had the same experience with an AI coding tool. The agent writes code quickly, the code often runs, and yet nobody on the team can say why it was written that way. Two sessions later the intent is gone, buried in a chat history, and the next change starts from a guess.&lt;/p&gt;

&lt;p&gt;I call this vibe coding. I do not think the problem is the model. The problem is where the context lives. If the intent exists only in a prompt, it disappears when the session ends. If it exists in a document that the agent reads and the human reviews, the team can inspect it, change it, and keep it.&lt;/p&gt;

&lt;p&gt;SDD, for me, comes down to four points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The intent is written down before the code.&lt;/strong&gt; In a file, not in a chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent works inside that intent.&lt;/strong&gt; It does not reinterpret it silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The human approves the intent and judges the result.&lt;/strong&gt; The agent does not own the definition of done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec is versioned with the code.&lt;/strong&gt; When the intent changes, the change is visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As in the first article, I would say the rigor of the spec can vary (spec-first, spec-anchored, spec-as-source), and these four points hold at every level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; the framework is a way to apply the idea at scale, not the idea itself.&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%2Fr68mka5gbcj5s8pjxato.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%2Fr68mka5gbcj5s8pjxato.png" alt=" " width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  BMAD in a Nutshell
&lt;/h2&gt;

&lt;p&gt;BMAD stands for Breakthrough Method of Agile AI-Driven Development. It maps familiar roles (analyst, product manager, UX designer, architect, developer) onto AI agents, and it keeps the work in documents that the agents read and the humans review.&lt;/p&gt;

&lt;p&gt;Two ideas matter most for me. The first is that each document has one owner and one writing skill. The Product Requirements Document (PRD) is the source of the product definition, and nothing else claims to define it. The planning docs say that documents are "contracts between people first and input to the skills second". The second idea is a rule: nothing downstream reinterprets the PRD. If a story needs an answer the PRD does not have, the answer goes into the PRD first, and the derived documents are regenerated. This is the whole point of the design.&lt;/p&gt;

&lt;p&gt;The other thing BMAD gives you is more than one path. The first decision is about size, not tools. A change of roughly 500 lines, across a few files, fits one build session. If the intent is already well defined, you hand it to a build, or to the epics and stories skill when the work is bigger. If it is not, you use the analysis skills first, in any order, until it is. The planning skills write the requirements down; they do not decide what you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trivial, you review it yourself:   edit directly, no process
One session (about 500 lines):     intent -&amp;gt; /bmad-build -&amp;gt; result
Epic-sized:                        intent -&amp;gt; brief, PRD, UX, architecture, epics and stories -&amp;gt; one /bmad-build per story -&amp;gt; retrospective
Project-sized:                     several epics, shared decisions first, then the epic path for each
&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%2Fyz9cj5f2y8kj044lo1b1.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%2Fyz9cj5f2y8kj044lo1b1.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The skills are grouped by phase: Analysis (optional, any order), Planning (PRD and UX), Solutioning (architecture, epics and stories, sprint status) and Build (one story at a time). One note: the version I have installed (6.12.4) labels its skills only plan and ship, so the four names are the grouping of the planning guide, not a label you will see in the tool.&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%2Fczbke6it3r8o7qmva1bf.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%2Fczbke6it3r8o7qmva1bf.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; size the process to the work, and make sure the intent is clear before anything is built.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Is Built This Way
&lt;/h3&gt;

&lt;p&gt;The BMAD docs describe what each piece is and how to use it. They say little about why it is built this way, so part of what follows is my reading, and I mark it as such.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents carry the whole picture across sessions.&lt;/strong&gt; The planning guide says that the PRD, the architecture and each ticket's requirements "exist so later sessions can still see the whole", and it warns that "Dividing work can lose information". That is the trade: the work is cut into one-session units, and the documents keep the pieces from drifting apart. Each run starts in a fresh chat for a reason the build docs give: "Reusing a session from another workflow can mix contexts and confuse the run."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared decisions are written once, where they are shared.&lt;/strong&gt; The stated purpose of the architecture step is: "Make the technical decisions that keep separately built parts consistent." I think this is why the architecture stays short: it records what several builders must follow and leaves the rest to each story. The docs do not say "only", so this part is my reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human attention goes where a mistake gets copied.&lt;/strong&gt; The build docs say that early stories "often settle the architecture, initial project structure, and repeated patterns", and that these decisions deserve human attention "before automating repetitions of them". I think the same logic explains the gates on the PRD and the architecture: an error there is not fixed once, it is built into every story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I did not find: a reason for splitting the work across roles in the first place. The closest statement is about people, not agents: "You make the calls, so you understand what you ship." I would not claim more than that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; the documents are the memory, and the human gates sit where a mistake gets copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up
&lt;/h2&gt;

&lt;p&gt;The version I use is BMAD 6.12.4, the stable one. It installs from the project directory with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx bmad-method install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer writes the skills for the coding tool I choose (Claude Code in my case) into &lt;code&gt;.claude/skills/&lt;/code&gt;, and a &lt;code&gt;_bmad/&lt;/code&gt; folder with the configuration. The skills are typed in the slash form (&lt;code&gt;/bmad-build&lt;/code&gt;, &lt;code&gt;/bmad-prd&lt;/code&gt;), and that is the form I use here. When I am unsure where to start, there is &lt;code&gt;/bmad-help&lt;/code&gt;. The online docs now describe a newer v7 preview with some different names. I follow the version I use, and I say where the docs differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Example: a Todo List
&lt;/h2&gt;

&lt;p&gt;I use the same todo list as in the Spec Kit series, so the two approaches can be compared on the same problem. There are two situations, and they lead to two different paths. In both I show the commands and not the results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path One: Adding a Flag to a Todo List I Already Have
&lt;/h3&gt;

&lt;p&gt;Suppose the todo list is already implemented, with add, edit and delete. I want to add a "done" flag to each item and toggle it from the list view. The change is small, clear and low risk, and it fits in one session. For this path I need only one skill: &lt;code&gt;bmad-build&lt;/code&gt;. It takes an intent, investigates, plans, implements, reviews, and fixes what it finds.&lt;/p&gt;

&lt;p&gt;Below is the flow from the build documentation (docs.bmad-method.org/build/build-a-change/), redrawn in my own style. The main line runs from top to bottom: intent, clarify, plan, implement, review, result. Dashed lines show where a human takes part, and the loops are the interesting part.&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%2Fe2bxhcbg1tvwvxb2ldzg.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%2Fe2bxhcbg1tvwvxb2ldzg.png" alt=" " width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Size the Work and Start Fresh&lt;/strong&gt; (one session, no shared context):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-build Add a "done" flag to each todo item, toggle it from the list view, and keep the state after reload.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change touches a few files and far fewer than 500 lines, so &lt;code&gt;bmad-build&lt;/code&gt; is the right tool. A trivial change that I will review myself does not need the process at all. I run the command in a new chat, because reusing a session from another workflow can mix contexts, and that is where wrong assumptions come from. The intent can be rough, and it can also be an issue link or a story (&lt;code&gt;/bmad-build story 2.3&lt;/code&gt;, which I use in Path Two). I prefer to write it in full. A rough intent is allowed, but it moves the cost to the questions the agent asks later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Clarify and Plan&lt;/strong&gt; (the human answers, the plan is the optional gate):&lt;/p&gt;

&lt;p&gt;The agent reads the codebase and any planning documents. Anything the repository cannot settle becomes an open question, and I answer carefully, because wrong answers here are the costliest to fix later. In the todo case I would expect a question about whether done items stay in the list. That is my decision, not the agent's.&lt;/p&gt;

&lt;p&gt;Then the agent checks three facts: intent gaps, irreversible actions, and the footprint of the change. If all three are clean, it takes the light path: a minimal plan and the implementation in the same session. If anything is flagged, it writes a full plan and waits for my approval. Fixing the plan is cheaper than fixing the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Implement and Review&lt;/strong&gt; (the agent's own gates):&lt;/p&gt;

&lt;p&gt;The agent implements, checks the change against the acceptance criteria it recorded, and reviews its own work with three lenses: a Blind Hunter (problems without the bias of the implementation context), an Edge Cases Hunter (forgotten corner cases) and a Verification Gap Finder (do the tests really cover the change?). The review ends in a triage, and each outcome has a destination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patch.&lt;/strong&gt; The issue belongs to this change, so it is fixed and the review runs again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad plan.&lt;/strong&gt; The code is wrong because the plan was weak, so the flow returns to Plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent gap.&lt;/strong&gt; The plan is wrong because the goal was wrong, so it returns to Clarify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Void.&lt;/strong&gt; The change is rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defer.&lt;/strong&gt; An unrelated pre-existing issue goes to &lt;code&gt;deferred-work.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like that the loops are explicit. A review that can only say fixed or not fixed hides the cause of the problem, and here the cause has to be named.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Present and Review the Result&lt;/strong&gt; (the human gate):&lt;/p&gt;

&lt;p&gt;The agent gives a short summary and commits locally. I run the walkthrough or skim the diff, tell the agent what to fix, and when I am satisfied I ask it to push the commit and open the PR. Two things can still go wrong after this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the change turns out to be wrong after I accepted it.&lt;/strong&gt; If I pushed the commit and then find a problem, the way back is the ordinary one in git, so the history stays intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git revert HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a new commit that cancels exactly what the agent added. I do not patch the problem inside the old chat. I start a fresh chat and run &lt;code&gt;/bmad-build&lt;/code&gt; again, with the intent rewritten to say what I missed (for instance: "the flag must survive a page reload"). The intent is the thing I correct, and the code follows it. This is a git practice that fits the flow, not a step that BMAD runs for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the agent chose not to fix.&lt;/strong&gt; An unrelated problem is not fixed silently and not dropped: it is written into &lt;code&gt;deferred-work.md&lt;/code&gt;, with the story it came from, a one-line summary and the evidence. I read this file after every run. A backlog nobody reads is where problems go to be forgotten, so for each entry I decide whether it is a new change, a note to keep, or something to drop on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path Two: Writing a Todo List From Scratch
&lt;/h3&gt;

&lt;p&gt;Now suppose there is no todo list yet. I want a todo app with due dates and filters, kept in the browser. This is a body of work with several build units, so one &lt;code&gt;/bmad-build&lt;/code&gt; is not enough. The planning documents become the contract, and each step reads the files that the previous step wrote. Every step runs in a fresh chat (&lt;code&gt;/clear&lt;/code&gt; first). I show the real commands. What each step reads and writes is in one table, a few sections below.&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%2F4td2l567sdh3kwi2a889.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%2F4td2l567sdh3kwi2a889.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Brainstorm&lt;/strong&gt; (optional, what and why, no technology):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-brainstorming A simple todo list web app. Explore features, UX ideas and what must stay out of version 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A coach runs the session in the stance I choose: facilitator, creative partner, or ideate for me. My decisions are kept apart from the ideas the coach offered. If the idea is still half-formed, &lt;code&gt;/bmad-forge-idea&lt;/code&gt; pressure-tests it and &lt;code&gt;/bmad-deep-recon&lt;/code&gt; researches a competitor or a technology. A personal view, not a verdict: if I cannot write the intent in three sentences after this step, I am not ready for a PRD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Product Brief&lt;/strong&gt; (what and why, in one page):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-product-brief Create a product brief for a simple todo list web app. Use the brainstorming result at &amp;lt;intent path&amp;gt; as the main input and treat its "Decisions" table as settled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill asks whether I want to create, update or validate, and whether I take the Fast path or the Coaching path. Every planning skill has the same three intents, and I run validate in a fresh chat before moving on (&lt;code&gt;/bmad-product-brief validate the brief at &amp;lt;brief path&amp;gt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: PRD&lt;/strong&gt; (what to build, not how):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-prd Create the PRD for the todo list using the brief at &amp;lt;brief path&amp;gt; and the brainstorming intent at &amp;lt;intent path&amp;gt;. Features in order: (F0) foundations and storage; (F1) the list: add, tick, delete; (F2) due dates, overdue sign and filters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PRD is the source of the product definition, and every later document derives from it. Functional requirements get stable ids (FR-1, FR-2, ...), and what the skill inferred is tagged &lt;code&gt;[ASSUMPTION]&lt;/code&gt; so that I can correct it. Edits belong in the PRD, not in the stories (&lt;code&gt;/bmad-prd update the PRD at &amp;lt;prd path&amp;gt;: &amp;lt;what to change&amp;gt;&lt;/code&gt;), so the derived documents can be generated again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: UX&lt;/strong&gt; (optional, how it looks and how it behaves):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-ux Create the UX documents for the todo list from the PRD at &amp;lt;prd path&amp;gt;. Simple, calm, keyboard friendly, one screen.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill captures my vision and does not impose its own. It writes two peer documents: &lt;code&gt;DESIGN.md&lt;/code&gt; (how it looks) and &lt;code&gt;EXPERIENCE.md&lt;/code&gt; (how it behaves). I would skip it only if I accept designing those details later, during the builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Architecture&lt;/strong&gt; (how, only where two builders could disagree):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-architecture Create the architecture for the todo list from the PRD at &amp;lt;prd path&amp;gt;, DESIGN.md and EXPERIENCE.md at &amp;lt;ux folder&amp;gt;. Decide: front-end framework, where tasks are stored, the task data model, the testing strategy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is not a full technical document. It is a short architecture spine that records only the decisions that two units, built independently, could make in incompatible ways. A decision gets a stable id (AD-1, AD-2, ...) only if it is non-obvious and a real trade-off. This is where my review matters most, because a wrong shared decision spreads to every story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Epics and Stories&lt;/strong&gt; (work cut by user value):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-create-epics-and-stories Create the epics and stories list from the PRD, UX documents and architecture. Create exactly three epics, in this order: Epic 1 Foundations; Epic 2 Task list; Epic 3 Due dates and filters. Every story needs acceptance criteria.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every functional requirement is mapped to an epic, and every story has acceptance criteria in the Given, When, Then form and fits one build session. I check the epic and story numbers, because every build command from now on uses them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Readiness Check&lt;/strong&gt; (the gate before the build):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-sprint-planning check implementation readiness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill reads the planning documents the way a skeptic reads a handoff, and answers PASS, CONCERNS or FAIL. On CONCERNS or FAIL I fix the planning documents and run the check again. If something major changes later, there is &lt;code&gt;/bmad-correct-course &amp;lt;description of the change&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Sprint Status&lt;/strong&gt; (one file that says where every story is):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-sprint-planning run sprint planning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes &lt;code&gt;sprint-status.yaml&lt;/code&gt;, with every epic, story and retrospective as a key. All stories start as &lt;code&gt;backlog&lt;/code&gt;, then move to &lt;code&gt;ready-for-dev&lt;/code&gt;, &lt;code&gt;in-progress&lt;/code&gt;, &lt;code&gt;review&lt;/code&gt; and &lt;code&gt;done&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Build One Story at a Time&lt;/strong&gt; (the implementation phase):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-build story 1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/bmad-build&lt;/code&gt; does not pick the next story by itself: I give it the number each time, one story per fresh chat, in order. Each run follows the flow of Path One. The story moves to &lt;code&gt;review&lt;/code&gt; in the sprint status, and the agent does not close its own work: I review, and I decide when a story is done. An optional extra check, in a fresh context, is &lt;code&gt;/bmad-code-review&lt;/code&gt;. I would not use the unattended build skill for the first stories, because they set the patterns for the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10: Retrospective&lt;/strong&gt; (close the epic):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bmad-retrospective 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill reads the evidence the epic left (the specs, the diff, the commits, the sprint status) and judges the epic against the criteria it set for itself. Every finding carries a source, and a claim with no source is dropped. A failing epic never closes as quietly accepted.&lt;/p&gt;

&lt;p&gt;Looking at these ten steps together, the pattern is always the same. Each skill writes one document or one piece of code. Each document has one writer. Each gate has a human decision. The agent works inside the boundaries that the earlier documents set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BMAD Writes to Disk
&lt;/h2&gt;

&lt;p&gt;What makes the process work is the files, because each step reads what the previous one wrote. BMAD keeps two folders at the project root, and they have different jobs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_bmad/&lt;/code&gt; is the metadata.&lt;/strong&gt; Installed version and modules, the skill list, the configuration, the team overrides, and the script that writes the logs. It holds no deliverable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_bmad-output/&lt;/code&gt; is the handoffs.&lt;/strong&gt; Every document a skill produces, in one run folder per document, plus the sprint status and the build specs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the handoff chain in one table: what each step reads, and what it writes for the next one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Reads&lt;/th&gt;
&lt;th&gt;Writes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brainstorm&lt;/td&gt;
&lt;td&gt;nothing (the idea is in the command)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brainstorm-intent.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brief&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brainstorm-intent.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;brief.md&lt;/code&gt;, &lt;code&gt;addendum.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRD&lt;/td&gt;
&lt;td&gt;brief, addendum, intent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prd.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UX&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prd.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DESIGN.md&lt;/code&gt;, &lt;code&gt;EXPERIENCE.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;PRD, &lt;code&gt;DESIGN.md&lt;/code&gt;, &lt;code&gt;EXPERIENCE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ARCHITECTURE-SPINE.md&lt;/code&gt;, plus &lt;code&gt;SOLUTION-DESIGN.md&lt;/code&gt; in coaching mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epics and stories&lt;/td&gt;
&lt;td&gt;PRD, UX documents, spine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;epics.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readiness check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;epics.md&lt;/code&gt; and the documents behind it&lt;/td&gt;
&lt;td&gt;PASS, CONCERNS or FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sprint status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;epics.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sprint-status.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build, per story&lt;/td&gt;
&lt;td&gt;the story, the spine, UX documents for UI work&lt;/td&gt;
&lt;td&gt;code, &lt;code&gt;spec-N-M-*.md&lt;/code&gt;, &lt;code&gt;epic-N-context.md&lt;/code&gt;, &lt;code&gt;deferred-work.md&lt;/code&gt;, status moved to &lt;code&gt;review&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrospective&lt;/td&gt;
&lt;td&gt;the specs of the epic, git history, &lt;code&gt;sprint-status.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the retrospective and its decision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each run folder also holds a &lt;code&gt;.memlog.md&lt;/code&gt;. It is the memory of that document: an append-only log with one line per decision, assumption or change, written by a script and never edited by hand. This is the shape of one (the lines are illustrative, for the todo PRD):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- (decision) Working mode: Fast path, entry point Vision + Features
- (assumption) Selected filter survives a reload
- (event) PRD finalized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things follow from this design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fresh chat can continue.&lt;/strong&gt; A step reads files, not the history of the previous chat, so I can close the session and resume tomorrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every decision has a trace.&lt;/strong&gt; The memlog says what was decided and why, and which assumptions are still open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A change starts at the source.&lt;/strong&gt; If a story needs an answer the PRD does not have, the answer goes into the PRD first, and the later steps run again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BMAD keeps one memlog per document, not one for all the sessions. If I want the view across the steps, I would keep a small file for it.&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%2Foyi57gsy79mkuyg7wjhv.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%2Foyi57gsy79mkuyg7wjhv.png" alt=" " width="799" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Method Gives You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context that survives sessions.&lt;/strong&gt; Decisions live in files, so a new chat starts from documents, not from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control for decisions.&lt;/strong&gt; The PRD, the architecture and the stories live in git, next to the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictability.&lt;/strong&gt; I know the likely end state of a session, because it was written before the session started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several sizes of process.&lt;/strong&gt; A one-session change and a whole product use the same vocabulary with very different amounts of ceremony.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other side, with the same sincerity as before:&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Method Does Not Give You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness by default.&lt;/strong&gt; An agent can implement the wrong thing precisely. A good spec reduces guessing, it does not remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero overhead.&lt;/strong&gt; For a typo or a CSS tweak, even the short path is too much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freedom from handoff problems.&lt;/strong&gt; Agents with different roles can still make conflicting assumptions. The documents make the conflict visible, but a human has to resolve it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room in the context window.&lt;/strong&gt; Specs, architecture and stories consume context, so they must stay concise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A replacement for judgment.&lt;/strong&gt; The framework tells you when to stop and ask. It does not tell you what the product should be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Role of the Human
&lt;/h2&gt;

&lt;p&gt;In every path, the agent produces a lot, and the human is responsible for what is accepted. On the short path I answer the open questions, approve the plan when it is flagged, and review the result. On the long path there are moments where a human decision blocks the next step. These are the ones I meet in the version I use (the planning guide of the online docs lists five sign-off points for the newer version):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Moment&lt;/th&gt;
&lt;th&gt;What I judge&lt;/th&gt;
&lt;th&gt;What it blocks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brainstorm intent&lt;/td&gt;
&lt;td&gt;Which directions survive, the Decisions table&lt;/td&gt;
&lt;td&gt;The brief and the PRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRD validation&lt;/td&gt;
&lt;td&gt;The findings report&lt;/td&gt;
&lt;td&gt;UX and architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture validation&lt;/td&gt;
&lt;td&gt;The decisions that are mine, the corrections&lt;/td&gt;
&lt;td&gt;Epics and stories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readiness check&lt;/td&gt;
&lt;td&gt;PASS, CONCERNS or FAIL&lt;/td&gt;
&lt;td&gt;Sprint planning and the first build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan approval and story review&lt;/td&gt;
&lt;td&gt;The plan when flagged, the diff, &lt;code&gt;deferred-work.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Marking the story done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrospective decision&lt;/td&gt;
&lt;td&gt;Did the epic meet its criteria&lt;/td&gt;
&lt;td&gt;The next epic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these leaves a written trace, in a memlog, a report or a status file. In a regulated company that is the audit trail, and in a small team it is the best record of why a decision was made. I think this is the clearest statement of responsibility I have seen in an agent workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; bounded autonomy, not full automation.&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%2Fc9716lotf07u7f17e2ck.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%2Fc9716lotf07u7f17e2ck.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A spec is a contract between people first, and input for agents second.&lt;/strong&gt; If the intent is not clear to the people involved, no agent will make it clear for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size comes first.&lt;/strong&gt; One session, one epic and one project need different amounts of process, and choosing between them is my call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The loops in the build flow are the point.&lt;/strong&gt; A bad plan, an intent gap and a deferred problem each go back to the layer that caused them, so a wrong decision is not buried in the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The human approves the plan and judges the result, and the agent works inside both.&lt;/strong&gt; The responsibility for what is produced does not move. What changes is where I spend my attention: on the intent, the gates and the diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The handoff between steps is a file, not a chat.&lt;/strong&gt; That is what lets a fresh session continue, and what leaves a written trace of every decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;BMAD is one answer to a question that many people are asking at the same time: how do we give an agent enough written intent to work well, and keep a human in charge of what comes out? I do not think it is the only answer, and I would not want this series to be only about one tool.&lt;/p&gt;

&lt;p&gt;In the next article I want to make the example more concrete. I also want to keep the radar on, because spec-driven development is moving fast: what the major players are doing about it, how the SDLC changes when an agent does part of the work, and which other approaches are worth exploring. Two that I already have on my list are &lt;a href="https://github.com/Fission-AI/OpenSpec/" rel="noopener noreferrer"&gt;OpenSpec&lt;/a&gt; by Fission-AI, and the &lt;a href="https://github.com/addyosmani/agent-skills" rel="noopener noreferrer"&gt;agent-skills&lt;/a&gt; collection by Addy Osmani. I have not formed a view on them yet, and I would rather look before I write about them.&lt;/p&gt;

&lt;p&gt;There will be at least one more article after this one. Stay tuned.&lt;/p&gt;

&lt;p&gt;What is your experience with planning documents and agents? Do you keep the spec in the repo, in a tracker, or in the chat? I would love to read your experience in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bmad</category>
      <category>specdrivendevelopment</category>
    </item>
    <item>
      <title>Agents-skill</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Tue, 27 Oct 2026 05:51:03 +0000</pubDate>
      <link>https://dev.to/rosidotidev/agents-skill-3c03</link>
      <guid>https://dev.to/rosidotidev/agents-skill-3c03</guid>
      <description></description>
    </item>
    <item>
      <title>Spec-Driven Development in Practice: Porting RoomBook from Spring Boot to NestJS</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:09:23 +0000</pubDate>
      <link>https://dev.to/rosidotidev/spec-driven-development-in-practice-porting-roombook-from-spring-boot-to-nestjs-53dg</link>
      <guid>https://dev.to/rosidotidev/spec-driven-development-in-practice-porting-roombook-from-spring-boot-to-nestjs-53dg</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/rosidotidev/spec-driven-development-in-practice-building-roombook-with-github-spec-kit-cl2"&gt;second article&lt;/a&gt; I built RoomBook with Spec Kit: an Angular frontend, a Spring Boot backend, SQLite, and five feature cycles, each with its own spec, plan and tasks. I finished with a question: can the backend be replaced by another stack while the contract, the tests and the frontend stay fixed?&lt;/p&gt;

&lt;p&gt;This article is the answer I got, and the answer took more than one prompt. The new backend is written in &lt;strong&gt;NestJS&lt;/strong&gt; on Node.js with TypeScript, in &lt;code&gt;backend-nest/&lt;/code&gt;, next to the old one. Getting there meant a constitution amendment, a specification, a plan, four implementation phases with checkpoints, an amendment in the middle of the work, and a parity check against the original service. Everything ran on the same repository and under the same constitution, with one new feature folder for the migration.&lt;/p&gt;

&lt;p&gt;The toolkit is &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt;, and the whole project, with the specifications from the first two articles and the migration described here, is in my &lt;a href="https://github.com/rosidotidev/room_book/" rel="noopener noreferrer"&gt;room_book repository&lt;/a&gt;, under &lt;code&gt;specs/&lt;/code&gt;. As before, this is a personal account, and I quote the command outputs where they matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal, Stated as Constraints
&lt;/h2&gt;

&lt;p&gt;A migration has an easy version of the goal, "rewrite the backend in the new stack", and a hard version, "users must not notice". I took the hard one and wrote it down as constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The HTTP contract stays the one in &lt;code&gt;openapi.yaml&lt;/code&gt;, byte for byte.&lt;/li&gt;
&lt;li&gt;The business rules stay the ones already specified: no overlaps, opening hours, 15-minute slots, a 4-hour maximum, no bookings in the past, cancellation up to one hour before the start, and disabled rooms not bookable.&lt;/li&gt;
&lt;li&gt;Errors keep the &lt;code&gt;application/problem+json&lt;/code&gt; format and the same status codes.&lt;/li&gt;
&lt;li&gt;The data already in the SQLite file must still be readable.&lt;/li&gt;
&lt;li&gt;The Angular frontend is not modified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the work came from taking these lines seriously. If the frontend changes, the migration has failed, even when the new backend works perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Decisions Before the First Command
&lt;/h2&gt;

&lt;p&gt;Two decisions were written down before the first Spec Kit command, so that anyone running the process could see them.&lt;/p&gt;

&lt;p&gt;The first is about data. Two backends must never write to the same SQLite file at the same time, so the new service works on a &lt;strong&gt;copy&lt;/strong&gt; of &lt;code&gt;roombook.db&lt;/code&gt;. The original file stays where it is, untouched, and I keep its SHA-256 hash to prove it.&lt;/p&gt;

&lt;p&gt;The second is about the cutover. Removing &lt;code&gt;backend/&lt;/code&gt; and renaming &lt;code&gt;backend-nest/&lt;/code&gt; is a separate change, and I decided it should be an explicit choice, made after parity, not a step inside the feature. I wrote this into the specification as an assumption, so that no task could quietly remove the old backend.&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%2Fhphekzimaafp1wiluwm0.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%2Fhphekzimaafp1wiluwm0.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Constitution, as a Minor Amendment
&lt;/h2&gt;

&lt;p&gt;The constitution from the second article described a Spring Boot stack, Flyway and a monorepo with &lt;code&gt;backend/&lt;/code&gt;. A migration breaks some of those statements, so the first command amended the file instead of replacing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-constitution Amend the RoomBook constitution for the backend migration from Spring Boot
to NestJS. Changes: (1) Technical constraints: the backend is NestJS on Node.js LTS with
TypeScript, in backend-nest/ ... (2) Principle V: schema and seed changes go through versioned
SQL migrations, run by an internal runner; Flyway is no longer used. (3) Principle VII: backend/
remains only during the migration and is removed at cutover. All other principles stay unchanged.
Bump the version as a MINOR change and record the rationale in the Governance section.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what the prompt does not do: it does not rewrite the principles. Contract first, the injectable clock, the service-layer rules, the shared design system: all of them stay. The amendment only touches what the migration really changes, and the version number tells the reader that the change is a MINOR one. The rationale goes into the Governance section, so the reason is recorded where the next person will look.&lt;/p&gt;

&lt;p&gt;The check is simple: the file contains the new stack, the version went up, and the rationale is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Specify, Without a Single Framework Name
&lt;/h2&gt;

&lt;p&gt;The specification is the one place where I was strict. A specification for a migration is still a specification of behavior, so it must not mention the technology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-specify Replace the RoomBook backend with a new implementation in backend-nest/, without
changing visible behaviour. The HTTP contract defined in openapi.yaml stays unchanged and the
frontend is not modified. The business rules stay identical. Errors stay in application/problem+json
format with the same status codes. The data already present in backend/data/roombook.db must be
readable by the new backend through a copy of the file. No new feature is included.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is &lt;code&gt;specs/006-nest-backend-replacement/spec.md&lt;/code&gt;. The check I give myself for every spec is short: no stack names in the requirements, and no &lt;code&gt;[NEEDS CLARIFICATION]&lt;/code&gt; marker left behind. A migration makes this rule harder to keep, because the temptation to write "use the same Express middleware" is strong. I kept it out.&lt;/p&gt;

&lt;p&gt;The optional clarify step is where I wanted the questions most: how the copy is made, what happens with a database that has not been migrated yet, and how parity is checked. Those are the questions a migration hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Plan, the Place Where the Stack Enters
&lt;/h2&gt;

&lt;p&gt;Now the stack goes in, and it is the same idea I described in the first article: the spec says what, the plan says how.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-plan Backend in backend-nest/ with NestJS on Node.js LTS and TypeScript. Module structure:
rooms and bookings, each with a controller (HTTP mapping only), a service (business rules) and a
repository (SQLite access). Persistence with better-sqlite3 and the versioned SQL files V1–V4 reused...
Tests: Jest with supertest for the contract tests, which validate requests and responses against
specs/001-project-setup/contracts/openapi.yaml and every documented status code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in this prompt matter more than the rest.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;reuse of the migrations&lt;/strong&gt;. The SQL files V1 to V4 are copied as they are. The Spring service uses Flyway, which keeps its own history table, so the new service must verify that history without changing it. The plan therefore splits the job in two: a read-only check of Flyway's table at startup, and a separate table for future migrations (V5 and later). The research file records this as a decision, R-003, with its reason: the copy's history must not change.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;the clock&lt;/strong&gt;. Every time-dependent rule uses an injectable Clock, as the constitution asks. For the parity runs, the new service can be frozen with an environment variable, while the Spring service keeps the real clock. That sounds odd at first, but it makes the recorded requests robust against time: a request about "tomorrow at 10:00" means the same thing on both sides, and it does not depend on when the test runs.&lt;/p&gt;

&lt;p&gt;The planning step produced &lt;code&gt;plan.md&lt;/code&gt;, &lt;code&gt;research.md&lt;/code&gt;, &lt;code&gt;data-model.md&lt;/code&gt;, &lt;code&gt;contracts/README.md&lt;/code&gt; and &lt;code&gt;quickstart.md&lt;/code&gt;. The check I run on the plan is the one from the second article: no reference to Spring in the implementation choices. The Spring code is there only as a reference for the rules, and the plan says so.&lt;/p&gt;

&lt;p&gt;The plan also records the versions I checked on the day: NestJS 12, better-sqlite3 13 (which requires Node 22 or later) and Jest 30. The local runtime was Node 24. Writing the versions down is a small habit that saves a lot of confusion later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Checklist and Tasks, Test First
&lt;/h2&gt;

&lt;p&gt;The checklist is an optional gate. Spec Kit does not require it, and it was tempting to skip it, because the spec already comes with its own requirements checklist. I ran it anyway, with an explicit focus, because the questions this migration could hide are about compatibility, not about the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-checklist Quality of the parity requirements: unchanged contract, complete business rules,
error format, compatibility with the existing database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checklist does not change the spec and does not inspect the code. It produces questions about the requirements, written as if a reviewer had never seen the implementation: is every rule complete, is every error stated, is the database compatibility defined well enough to be tested? A person answers them. In this project the checklist belongs to the reviewer, so it is something I read and judge, not something I let the agent tick off. Before implementation, the requirements checklist had 16 items, all checked, and the implement command reads that state as a gate before it starts.&lt;/p&gt;

&lt;p&gt;Then came the tasks, which is where the method turned into test-first development. I chose TDD on purpose, and I wrote it into the order of the phases instead of leaving it to the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-tasks Order the tasks in phases: (1) scaffolding of backend-nest/ and the migration runner;
(2) contract tests for each endpoint, written before the code; (3) implementation of the rooms and
bookings modules; (4) parity comparison with the Spring backend on the same copied database.
No task modifies openapi.yaml.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four phases each have a precise role:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scaffolding and migration runner.&lt;/strong&gt; The runner's tests were written before the runner. The first red tests asked a simple question: does the service refuse to start on a missing or mismatched database?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract tests, red by design.&lt;/strong&gt; This phase has no controller and no service. Every endpoint and every documented status code gets a test that validates against &lt;code&gt;openapi.yaml&lt;/code&gt;, and all of them fail at the start. The implement instruction is explicit that they must fail because the feature is missing, not because of a setup error. A test that is red because of a broken import looks like progress, and it is not, so I read the failure messages myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation, test first again.&lt;/strong&gt; The booking rules were written as tests before the rules themselves. Only then did the rooms and bookings modules appear, and the tests turned green one at a time. The boundaries of the rules were checked with a fixed clock, so the results were deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parity.&lt;/strong&gt; The same requests are sent to both services, on identical copies of the database, and the responses are compared.&lt;/li&gt;
&lt;/ol&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%2Frtpvdkovjr78p14j3lzz.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%2Frtpvdkovjr78p14j3lzz.png" alt=" " width="799" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Phase 2 is the one I would point to. The contract tests were the specification in executable form, and they were red before a single line of the new backend existed. Later, when the amendment moved DTOs and transactions into their final places, the same suite was the check that nothing visible had changed.&lt;/p&gt;

&lt;p&gt;The analysis step, run before implementation, checked two things that I wanted to be sure of: no task touches &lt;code&gt;openapi.yaml&lt;/code&gt;, and every endpoint has a contract test. It found no critical inconsistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Implement and Converge, One Phase at a Time
&lt;/h2&gt;

&lt;p&gt;Each phase follows the loop from the second article: implement, converge, repeat until converge says "Converged", and check the phase checkpoint before moving on. The implement argument repeats the hard limits every time, because each run starts with a fresh context and I do not want to rely on memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-implement Only phase 2 (T021–T034). ... Do not implement any controller or service in this
phase. The new route tests must fail because the feature is missing, not because of a setup error.
Do not modify openapi.yaml, frontend/ or backend/.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sentence about the failing tests is the one I would copy into any migration. A test that fails for a setup error looks like progress, and it is not. I check the failure messages myself: an import error or a wrong path means the phase is not done.&lt;/p&gt;

&lt;p&gt;Some things I learned from the runs, which the tasks alone would not have shown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;import type&lt;/code&gt; for interfaces.&lt;/strong&gt; With decorators, &lt;code&gt;emitDecoratorMetadata&lt;/code&gt; and ts-jest in ESM mode, importing an interface as a normal value fails at load time. It was a small error, found by the first test run, and it is now a rule in the decision log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state in tests.&lt;/strong&gt; The overlap test in the booking contract file failed only when it ran after another test. Alone it passed. The failure came from a shared app and database, not from the rules. The fix gives that single test its own app and its own database, without changing any expectation: 201 for the first booking, 409 "Slot already booked" for the overlap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This second point is the one I want to underline. When a test fails, the first question is whether the code is wrong or the test is wrong. In this project, a test expectation that looks wrong must be reported, not edited, and the migration instructions say so explicitly.&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%2Fvbmu61udwe7oxiknoohv.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%2Fvbmu61udwe7oxiknoohv.png" alt=" " width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An Amendment in the Middle of the Migration
&lt;/h2&gt;

&lt;p&gt;Halfway through, I wanted two changes that had nothing to do with the migration's first goal. I wanted one DTO file per module, following the pattern of the earlier features, and I wanted database transactions to belong only to the service layer, as the constitution already says. Neither change alters the contract or the rules, but both alter the structure.&lt;/p&gt;

&lt;p&gt;I did not open a new feature for this. I used the existing one, with the same Spec Kit sequence, and each command was given an explicit folder so that nothing would be created by mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-specify Amendment to feature 006 (backend-nest replacement), no new user-facing behaviour: ...
/speckit-plan Amendment to the existing plan of feature 006 (do not create a new feature)...
/speckit-tasks Amendment to the existing tasks of feature 006... Append a new phase after the existing
tasks; do not renumber or edit existing completed tasks.
/speckit-implement Only phase 5 (T061–T068)...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results were a new requirement pair in the spec (FR-022 for the DTOs, FR-023 for transactions), an updated plan and data model, and a Phase 5 appended to &lt;code&gt;tasks.md&lt;/code&gt;. Existing tasks were not renumbered. That detail matters more than it looks: the task list is a log, and a log that gets rewritten is no longer a log.&lt;/p&gt;

&lt;p&gt;The amendment also gave me a useful example of how converge behaves. After the phase, converge found one high-severity gap, the shared-state overlap test I described above, and one medium gap: the parity gate had not been run yet. It did not fix either. It appended Phase 6, with one task per gap, and the loop closed them.&lt;/p&gt;

&lt;p&gt;Every decision taken without asking went into a decision table, with an ID, the decision, and a short reason. The table is short, so I can review it in two minutes. For example, &lt;code&gt;D-2&lt;/code&gt; moved the &lt;code&gt;BookingSummary&lt;/code&gt; type into the bookings DTO file rather than the rooms one, because its mapping belongs to the booking shape. That is the kind of choice I would have made silently in a normal project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Parity Gate, and Where I Stopped
&lt;/h2&gt;

&lt;p&gt;The last phase is the one that decides whether the migration is real: the same requests, sent to the Spring service and to the NestJS service, on identical copies of the database, must produce identical responses.&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%2F5mxhdwkv4em6lzca81dk.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%2F5mxhdwkv4em6lzca81dk.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The harness has three rules. It resets both copies before each run, so a run cannot depend on a previous one. It uses a fixed set of recorded requests, 99 in this run, chosen to be robust against time. And it compares the status, the headers that matter and the body, byte by byte where the format is part of the contract.&lt;/p&gt;

&lt;p&gt;The gate command is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm --prefix backend-nest run parity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate ran to completion. Before the final run I re-recorded the Spring reference against the live Spring service, because the earlier reference file had been removed. The recording holds 99 requests. Then the gate replayed them on fresh copies of the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fresh copies made from backend/data/roombook.db (sha256 2e10fed8…cd44).
Compared 99 requests (Spring on http://127.0.0.1:8090, Nest on http://127.0.0.1:8091).
Differences: 0. Spring drift vs recorded: 0.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness starts both services itself, on ports 8090 and 8091. The ports in the early plan were 8080 and 8081, so the harness was adjusted to the run rather than the other way round.&lt;/p&gt;

&lt;p&gt;Zero differences and zero drift are the result that matters. The checks around it are green too: the NestJS suite passes 15 of 15 suites and 72 of 72 tests, &lt;code&gt;openapi.yaml&lt;/code&gt; shows no diff, the &lt;code&gt;frontend/&lt;/code&gt; and &lt;code&gt;backend/&lt;/code&gt; folders show no diff, and the SHA-256 of the original database is unchanged. With this run, the last two tasks, T060 and T070, are done. The cutover, removing &lt;code&gt;backend/&lt;/code&gt; and renaming &lt;code&gt;backend-nest/&lt;/code&gt;, remains a separate decision, as the specification requires (FR-021).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Process Gave Me
&lt;/h2&gt;

&lt;p&gt;After a second project with Spec Kit, here is what I would add to the list from the previous articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A migration becomes a checklist of constraints.&lt;/strong&gt; Writing "no change to the contract, the rules, the errors and the frontend" forced every later step to name what it must not touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests that fail for the right reason.&lt;/strong&gt; The phase 2 instruction is the most valuable sentence in the whole set of implement instructions. A red test is only progress when it is red for the expected reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions are visible.&lt;/strong&gt; The decision tables, the &lt;code&gt;R-&lt;/code&gt; and &lt;code&gt;D-&lt;/code&gt; entries, made it possible to review what the agent decided on its own, instead of finding it in the code later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The contract as a fixed point.&lt;/strong&gt; A SHA-256 check before and after, and a rule in every prompt, kept &lt;code&gt;openapi.yaml&lt;/code&gt; untouched through the four phases and the amendment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Process Taught Me
&lt;/h2&gt;

&lt;p&gt;The other side, with the same sincerity as before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The parity set is a deliberate, bounded choice.&lt;/strong&gt; For this exercise, 99 recorded requests were enough to cover the rules and error paths of the current API, and the gate compares the two services on each of them. In a production setting, the same harness could replay many more requests, including generated edge cases, because adding a case is just adding an entry to the file. The number is a starting point we chose, not a limit of the method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec is only as good as the questions asked.&lt;/strong&gt; Clarify found the copy strategy and the unmigrated-database case. It did not find, by itself, every behaviour that the old code had and nobody wrote down. That is why the parity harness matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good instructions take time to write.&lt;/strong&gt; Each command carried its own limits, checks and expected outputs, which is what made the process repeatable. Writing them was real work, and the cost is time, not tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A person still decides the cutover.&lt;/strong&gt; The agent can make the new backend pass its tests. It cannot decide that removing the old one is safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Constraints are the specification of a migration.&lt;/strong&gt; The fixed points, the contract, the rules, the errors and the frontend, are what make the change measurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase order is the safety net.&lt;/strong&gt; Scaffolding, then tests that fail for the right reason, then code, then parity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amendments belong to the existing feature.&lt;/strong&gt; A log with numbered tasks and decision tables is easier to trust than a rewritten one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passing tests are not parity.&lt;/strong&gt; The gate that matters compares two running services on the same data. Once it reports zero differences, the migration has evidence behind it, not only green tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The migration itself is in the repository: the specification and plan in &lt;code&gt;specs/006-nest-backend-replacement/&lt;/code&gt; and the code in &lt;code&gt;backend-nest/&lt;/code&gt;. The one decision left is the cutover, which is a separate change, and it is my call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;With this article, the series on GitHub Spec Kit comes to an end. The next one is about the BMAD Method. I am studying it right now, and writing about what I study is one of the best ways I know to learn it, especially when each concept is tied to a practical example from a real project situation. BMAD is similar in spirit to Spec Kit, but more complex: it has a steeper learning curve, and in exchange it covers a wider part of the software lifecycle. Stay tuned.&lt;/p&gt;

&lt;p&gt;Have you tried changing the stack of a project that has a contract, without touching the frontend? What did you keep fixed, and what did you find out only at the end? I would like to read your experience in the comments.&lt;/p&gt;

</description>
      <category>githubspeckit</category>
      <category>claude</category>
      <category>specdrivendevelopment</category>
      <category>ai</category>
    </item>
    <item>
      <title>Spec-Driven Development in Practice: Building RoomBook with GitHub Spec Kit</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:23:26 +0000</pubDate>
      <link>https://dev.to/rosidotidev/spec-driven-development-in-practice-building-roombook-with-github-spec-kit-cl2</link>
      <guid>https://dev.to/rosidotidev/spec-driven-development-in-practice-building-roombook-with-github-spec-kit-cl2</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/rosidotidev/spec-driven-development-with-github-spec-kit-3d2"&gt;first article&lt;/a&gt; I stayed on the theory: the specification as a contract, the Spec Kit workflow, a todo list as a toy example. This time I do what I promised and build something real. The application is called &lt;strong&gt;RoomBook&lt;/strong&gt;, a meeting-room booking app with an Angular frontend, a Spring Boot REST backend and SQLite persistence, built from the first line to the last one with &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt; and a coding agent.&lt;/p&gt;

&lt;p&gt;The whole project is on my GitHub: &lt;a href="https://github.com/rosidotidev/room_book/" rel="noopener noreferrer"&gt;room_book on Github&lt;/a&gt;. Every spec, plan, checklist and task list I mention below is in the repository, under &lt;code&gt;specs/&lt;/code&gt;. I show the method here and link the files for the rest, because a full walkthrough would be longer than anyone wants to read.&lt;/p&gt;

&lt;p&gt;I wrote this article with two goals. The first is to show what Spec Kit does, step by step, on a project with real business rules. The second is to be honest about what it gives you and what it does not. As in the first article, this is a personal view, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Example: RoomBook
&lt;/h2&gt;

&lt;p&gt;Why a room booking app? Because it looks trivial and is not. A booking is just a room, a title, a person and two timestamps, but the rules around it are exactly the kind of thing an AI agent gets subtly wrong when nobody writes them down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No overlap on the same room, but back-to-back bookings are allowed (10:00-11:00 and 11:00-12:00).&lt;/li&gt;
&lt;li&gt;Allowed hours 08:00-20:00, Monday to Friday, on 15-minute boundaries.&lt;/li&gt;
&lt;li&gt;Maximum duration 4 hours, and no bookings in the past.&lt;/li&gt;
&lt;li&gt;Cancellation allowed until one hour before the start, as a soft delete.&lt;/li&gt;
&lt;li&gt;A deactivated room cannot be booked, and its future bookings are reported, not deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Authentication, notifications and recurring bookings are explicitly out of scope. Saying what is &lt;strong&gt;not&lt;/strong&gt; in the project is as important as saying what is.&lt;/p&gt;

&lt;p&gt;I split the work into five macro features, and each one is a complete Spec Kit cycle:&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%2Fjtt2p6att8rgjf6dzeo5.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%2Fjtt2p6att8rgjf6dzeo5.png" alt=" " width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first three (setup, rooms API, rooms UI) are the base. Then comes a pure &lt;strong&gt;backend feature&lt;/strong&gt;, the bookings API, and a pure &lt;strong&gt;frontend feature&lt;/strong&gt;, the bookings UI. These two are the interesting ones, because they show how Spec Kit behaves when a project already has a constitution, a contract and a design system, and you only extend them.&lt;/p&gt;

&lt;p&gt;This is what the result looks like. The day view on desktop, with several rooms side by side:&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%2Fyvb583tdx8zqmtknk6aj.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%2Fyvb583tdx8zqmtknk6aj.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The room administration screen, where an inactive room is marked with a label and an icon, not only with a different color:&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%2F9kjx66svybgf3l8hmgvi.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%2F9kjx66svybgf3l8hmgvi.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these screens was designed in a mockup tool. They come from a specification, and that is what the rest of the article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Init and Constitution
&lt;/h2&gt;

&lt;p&gt;Spec Kit is installed and initialized from the terminal. These are the only terminal commands of the whole process, everything else is typed in the chat of the coding agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv tool install specify-cli
specify init roombook --integration &amp;lt;your-agent&amp;gt;
cd roombook
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository has to be under Git, because Spec Kit creates numbered feature branches. After the init, the agent gets the Spec Kit commands as skills, and you find them in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/.claude/skills/speckit-specify/SKILL.md
&amp;lt;prj_root&amp;gt;/.claude/skills/speckit-plan/SKILL.md
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the constitution, written once. This is the prompt I used, shortened here (the full text is in the repository):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-constitution Define principles for RoomBook, a meeting-room booking app with an
Angular frontend, a Spring Boot REST backend and SQLite persistence. Principles:
(1) API-first: the OpenAPI document is the source of truth and is changed before any code.
(2) Contract tests are mandatory for every endpoint.
(3) Business rules live in the service layer, never in controllers or in the frontend.
(4) Time-dependent logic must use an injectable Clock so tests are deterministic.
...
(10) The UI follows one shared design system based on design tokens, defined once and
never hard-coded in components.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a file of eleven principles. Here are a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/.specify/memory/constitution.md

### I. API-First (NON-NEGOTIABLE)
The OpenAPI document is the single source of truth for the HTTP API. Any API change MUST be
made in the OpenAPI document before any backend or frontend code is written.

### IV. Deterministic Time
All time-dependent logic MUST use an injectable `Clock`. Code MUST NOT call `now()` without a
clock. Tests MUST use a fixed clock so results are deterministic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to stress why this file matters. Every later plan is evaluated against it, and every deviation has to be justified in the plan itself. When the agent writes the booking rules, it does not decide on its own how to handle time: the constitution already decided. That is a piece of architecture, written once, enforced many times.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Cycle, Five Times
&lt;/h2&gt;

&lt;p&gt;Each macro feature follows the same sequence, which is the one from the first article with the three optional gates switched on:&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%2F1t8rkg4okgfp8nf58jqo.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%2F1t8rkg4okgfp8nf58jqo.png" alt=" " width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cycle is the same every time, and that is the point. I do not invent a new process for a backend feature and another one for a frontend feature. What changes is the content of the prompts: the specification talks about behavior, the plan talks about technology. And I review the output of every step before running the next one.&lt;/p&gt;

&lt;p&gt;Let me show the two interesting cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend Feature: Bookings API
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Specify&lt;/strong&gt; (what and why, no technology):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-specify Let employees book meeting rooms without double bookings. A booking has a
room, a title, the organizer's name, a start and an end time. Two bookings on the same room
cannot overlap, but one may start exactly when another ends. Bookings are allowed from 08:00
to 20:00, Monday to Friday, on 15-minute boundaries, last at most 4 hours and cannot be in the
past. A booking can be cancelled until one hour before it starts; cancelling frees the slot
and keeps the history. A deactivated room cannot be booked...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no Spring Boot, no SQL and no endpoint in this text. It describes rules a business person could read and correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Clarify&lt;/strong&gt; (find the holes before planning):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-clarify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where I started to see real value. The agent asked targeted questions about things I had not said, and folded the answers back into the spec. These are some of them, copied from the generated file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/004-bookings-api/spec.md

## Clarifications
- Q: Which time zone applies? -&amp;gt; A: A single office time zone, Europe/Rome by default and
  configurable; hours, weekdays and 15-minute boundaries are evaluated in the office time zone.
- Q: Can a booking span midnight? -&amp;gt; A: No; start and end must fall on the same office-local day.
- Q: Is cancellation allowed at exactly one hour before the start? -&amp;gt; A: Yes; it is allowed
  when at least 60 minutes remain before the start and refused when fewer remain.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had never written a word about time zones. Without this step, the agent would have guessed one, silently, and I would have found out in production. With it, the question became a decision with my name on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Plan&lt;/strong&gt; (how, with the contract first):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-plan Extend openapi.yaml first: booking schemas, the four endpoints and the
400/404/409/422 problem+json responses. Add a Flyway migration for the booking table with a
foreign key to room and an index on (room_id, start_at). The booking service validates all
rules using the injectable Clock; overlap check and insert in a single transaction...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the technology enters, and the first article's rule applies: the spec says what, the plan says how. Note the order in the prompt: the contract is extended &lt;strong&gt;before&lt;/strong&gt; any code, as the constitution requires. In the generated contract, even the order in which rules are checked is documented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-project-setup/contracts/openapi.yaml

/api/bookings:
  post:
    operationId: createBooking
    description: &amp;gt;
      Rules, checked in this order after the fields are present: unknown room (404),
      deactivated room (422), hours 08:00-20:00 office time Monday to Friday on 15-minute
      boundaries, same local day, duration of at most 4 hours, end after start, start not
      before now (400), then overlap with a confirmed booking of the same room (409).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Checklist&lt;/strong&gt; (quality of the requirements, not of the code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-checklist Requirements quality for booking rules: completeness, ambiguity and testability of each rule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checklist produced 42 items. They are questions about the requirements, which is why I find them useful: they read like a reviewer who has not seen the code. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/004-bookings-api/checklists/rules.md

- [ ] CHK005 Is "from 08:00 to 20:00" unambiguous about whether 20:00 is a valid end and 08:00 a valid start?
- [ ] CHK008 Are public holidays explicitly in or out of scope for "Monday to Friday"?
- [ ] CHK016 Is it stated who may cancel a booking, given there is no authentication, and that the organizer name is not a credential?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These checklists are reviewer-owned: the tool generates the questions, a human decides whether the answers are good enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Tasks, analyze, implement&lt;/strong&gt; (build it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-tasks
/speckit-analyze
/speckit-implement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task list for this feature has 28 tasks, each one naming the files it touches. &lt;code&gt;/speckit-analyze&lt;/code&gt; is read-only: it cross-checks spec, plan and tasks and reports conflicts and gaps before a single line of code is written. Then &lt;code&gt;/speckit-implement&lt;/code&gt; executes the tasks in dependency order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Converge&lt;/strong&gt; (the part I did not expect to like):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-converge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the moment I understood what "checked, not just read" means. After the first implementation, converge compared the code with the spec and found a gap. It added a new task under a Convergence section of &lt;code&gt;tasks.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/004-bookings-api/tasks.md

## Phase 9: Convergence
- [X] T028 Add rule tests in `backend/src/test/java/com/roombook/service/BookingRulesTest.java`
  with winter offsets and around the daylight-saving change of 25 October 2026 (Europe/Rome):
  the same office-local 08:00-20:00 window is accepted before and after the change although
  the UTC offset differs, 07:45 and 20:15 are still refused...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code worked. What was missing was a test for a requirement that came from the clarification about time zones. That is exactly the kind of gap that survives a code review, because nobody thinks about the last Sunday of October on a Tuesday afternoon. Converge did not edit the code: it appended a task, and the loop implement, then converge, closed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Feature: Bookings UI
&lt;/h2&gt;

&lt;p&gt;The frontend feature starts from a very different situation: the backend exists, the contract is published and the design system was created two features earlier. This changes the text of the prompts, and it is where I think Spec Kit shows its structure best.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specify&lt;/strong&gt; talks only about the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-specify Let employees see when a meeting room is free and book it from the browser.
They pick a room and a date and see a day view from 08:00 to 20:00 in 15-minute steps, with
booked periods showing the booking title... If the system refuses the booking, the message
explains why in plain language and the entered data is kept. The screens adapt to the device:
on a phone the day is a vertical agenda for one room with a date strip...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt; names the existing contract as a constraint, not as something to design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-plan Angular standalone components with routes /availability and /my-bookings,
reusing the layout, error-display component and generated client from F02. Do not modify
openapi.yaml; the contract from F03 is a fixed constraint... Reuse the design tokens, Material
theme and app shell from F02; add no new colors or fonts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sentence is the whole lesson. An agent that is told to "build the booking screens" might decide that a new endpoint would be convenient. An agent that is told the contract is fixed has to find a way inside it, or report a finding. And since the constraint is written down, it can be verified: the feature folder contains a hash of the contract taken before the work started, and the same hash was checked at the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/005-bookings-ui/contract-baseline.sha256
664521af719673603dd80717031c4518870d3e9c8f1281633009919aa68353fe

$ sha256sum specs/001-project-setup/contracts/openapi.yaml
664521af719673603dd80717031c4518870d3e9c8f1281633009919aa68353fe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same hash, same contract. The frontend did not touch it.&lt;/p&gt;

&lt;p&gt;The clarify step was again useful for decisions I would have left to chance. Two of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/005-bookings-ui/spec.md

- Q: What happens when a selection spans an occupied period? -&amp;gt; A: Only the free part up to
  the occupied period is selected; the occupied period is never included.
- Q: How is the organizer name remembered between visits? -&amp;gt; A: It is stored on the device (in
  the browser) and pre-filled in the booking form; it is never sent anywhere except as the
  organizer of a booking or as the name to look up.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adaptive behavior is also in the specification, so it is verified like everything else. The same page changes layout at the breakpoints defined in the constitution, 600 and 1024 pixels. On a tablet, a timeline for one room:&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%2Felh46pvrn9uvo4in3cfg.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%2Felh46pvrn9uvo4in3cfg.png" alt=" " width="768" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a phone, a vertical agenda with a date strip and a bottom navigation bar:&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%2Fccnsk0vkyatzwbaiup31.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%2Fccnsk0vkyatzwbaiup31.png" alt=" " width="500" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here too the convergence step found gaps and wrote them as tasks: component tests for the layout at each breakpoint with a stubbed breakpoint observer, and tests for the date strip and the responsive dialog. Nothing was missing in the screens. What was missing was the proof that they behave as specified.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spec Kit Leaves in the Repository
&lt;/h2&gt;

&lt;p&gt;Before looking at the code, it is worth looking at what the process produced next to it. Every macro feature has its own numbered folder under &lt;code&gt;specs/&lt;/code&gt;, created by &lt;code&gt;/speckit-specify&lt;/code&gt; and filled by the commands that follow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/
├── .specify/memory/constitution.md      &amp;lt;- the eleven principles, written once
├── specs/
│   ├── 001-project-setup/               &amp;lt;- F00, also holds the shared contracts/openapi.yaml
│   ├── 002-rooms-api/                   &amp;lt;- F01
│   ├── 003-rooms-ui/                    &amp;lt;- F02
│   ├── 004-bookings-api/                &amp;lt;- F03
│   └── 005-bookings-ui/                 &amp;lt;- F04
├── backend/
└── frontend/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The five folders have the same shape. This is the one of the bookings API, with the command that created each file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/004-bookings-api/
├── spec.md              &amp;lt;- specify + clarify (what and why, with the Clarifications)
├── plan.md              &amp;lt;- plan (how: stack, constraints, structure)
├── research.md          &amp;lt;- plan (decisions and rejected alternatives)
├── data-model.md        &amp;lt;- plan (entities, fields, constraints)
├── contracts/           &amp;lt;- plan (notes on the changes to the shared openapi.yaml)
├── quickstart.md        &amp;lt;- plan (how to run and validate the feature by hand)
├── checklists/
│   ├── requirements.md  &amp;lt;- specify (quality of the spec)
│   └── rules.md         &amp;lt;- checklist (the 42 questions)
└── tasks.md             &amp;lt;- tasks, then updated by implement and converge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend feature &lt;code&gt;005-bookings-ui&lt;/code&gt; has the same files plus two that exist only there: &lt;code&gt;contract-baseline.sha256&lt;/code&gt;, the hash of the contract taken before the work, and &lt;code&gt;contrast.md&lt;/code&gt;, the check of the colors against the accessibility rules of the constitution.&lt;/p&gt;

&lt;p&gt;I like this layout for one reason: each file answers one question, and the question has an owner. &lt;code&gt;spec.md&lt;/code&gt; is for the business, &lt;code&gt;plan.md&lt;/code&gt; and &lt;code&gt;data-model.md&lt;/code&gt; for the architect, &lt;code&gt;checklists/&lt;/code&gt; for the reviewer and &lt;code&gt;tasks.md&lt;/code&gt; for whoever runs the implementation. When a rule looks wrong in the code, I know in which file to look for the sentence that caused it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model&lt;/strong&gt;: the &lt;code&gt;specs/&lt;/code&gt; folder is the memory of the project. The code can be regenerated, this folder cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Built: Backend and Frontend
&lt;/h2&gt;

&lt;p&gt;The specs describe the project, and here is what they produced. The two applications are small, and their structure follows the plans closely.&lt;/p&gt;

&lt;p&gt;The backend is Spring Boot on Java 21, with SQLite and Flyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/backend/src/main/java/com/roombook/
├── api/         BookingController, RoomController, HealthController, DTOs, ApiExceptionHandler
├── service/     BookingService, BookingRules, RoomService, one exception per business rule
├── repository/  BookingRepository, RoomRepository
└── config/      ClockConfig, OfficeZoneConfig, DataDirectoryInitializer

&amp;lt;prj_root&amp;gt;/backend/src/main/resources/db/migration/
├── V2__create_room.sql
├── V3__seed_demo_rooms.sql
└── V4__create_booking.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layers&lt;/strong&gt;: controller -&amp;gt; service -&amp;gt; repository, with no business rule in the controllers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules in one place&lt;/strong&gt;: &lt;code&gt;BookingRules&lt;/code&gt; holds the checks on hours, boundaries and duration, and &lt;code&gt;BookingService&lt;/code&gt; adds the overlap check and the transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors as types&lt;/strong&gt;: one exception per rule (&lt;code&gt;BookingConflictException&lt;/code&gt;, &lt;code&gt;CancellationTooLateException&lt;/code&gt;, &lt;code&gt;RoomDeactivatedException&lt;/code&gt;), mapped to problem+json by &lt;code&gt;ApiExceptionHandler&lt;/code&gt;, as the contract says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time as a dependency&lt;/strong&gt;: &lt;code&gt;ClockConfig&lt;/code&gt; and &lt;code&gt;OfficeZoneConfig&lt;/code&gt; inject the clock and the office time zone, so the tests can fix both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: one Flyway migration per feature, nothing created by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt;: the &lt;code&gt;contract/&lt;/code&gt; package has one class per group of endpoints and checks the responses against &lt;code&gt;openapi.yaml&lt;/code&gt;, while the &lt;code&gt;service/&lt;/code&gt; package covers the rules, concurrency and performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend is Angular with standalone components and Angular Material:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/frontend/src/app/
├── api/         client generated from openapi.yaml, never edited by hand
├── bookings/    availability.page, day-view, date-strip, booking-form, my-bookings.page, bookings.facade
├── rooms/       room-list.page, room-admin.page, room-card, room-form, deactivate-dialog, rooms.facade
├── layout/      shell and page components (the app shell)
├── shared/      problem-message, responsive-dialog, office-time, clock, organizer-store
└── styles/      tokens.css, theme.scss (the design system)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generated client&lt;/strong&gt;: &lt;code&gt;api/&lt;/code&gt; comes from the contract, so the frontend cannot call an endpoint that does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages and facades&lt;/strong&gt;: in the bookings and rooms areas, each page has a facade that wraps the generated client, so components do not talk to HTTP directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared pieces&lt;/strong&gt;: the error component turns a problem+json into a plain message, and the responsive dialog is a dialog on desktop and a full screen on a phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design tokens&lt;/strong&gt;: colors and breakpoints live in &lt;code&gt;styles/&lt;/code&gt;, as the constitution requires, and no feature adds its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The counts are small on purpose: 26 backend test classes and 16 frontend spec files, which hold the tests counted below. What matters is that the folders in the code mirror the folders in &lt;code&gt;specs/&lt;/code&gt;: a rule in a spec has a place in &lt;code&gt;service/&lt;/code&gt;, a screen in a spec has a page in &lt;code&gt;app/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Tool Gives You
&lt;/h2&gt;

&lt;p&gt;After building a real project with it, here is what I would put on the value side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Questions before code.&lt;/strong&gt; Clarify and checklist turn unstated assumptions into written decisions. This is, for me, the single biggest gain: the time zone, the midnight rule and the cancellation boundary were all decided by a person, not by a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability.&lt;/strong&gt; Every task points to a requirement, every requirement to a user story. When something looks wrong in the code, I can follow it back to a sentence in the spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A constitution that works.&lt;/strong&gt; Eleven principles, written once, shaped four features. Contract first, fixed clock, problem+json errors: I did not repeat them in a single feature prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gates that fail loudly.&lt;/strong&gt; Converge and analyze do not praise the work, they look for gaps. In my case they found missing tests, which is the kind of debt that normally stays invisible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A repeatable cycle.&lt;/strong&gt; The same eight commands for a backend rule engine and for a responsive grid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers of the project, for context: 36 tasks for the setup and between 27 and 28 for each of the four features, about 110 backend tests and 91 frontend tests, all generated from the tasks and the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Tool Does Not Give You
&lt;/h2&gt;

&lt;p&gt;It would be easy to stop at the list above, so let me add the other side, with the same sincerity as in the first article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A wrong spec produces wrong code, faithfully.&lt;/strong&gt; Converge says the code matches the spec. It cannot say the spec is what the business needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The review work does not disappear, it moves.&lt;/strong&gt; Reading a constitution, a spec, a plan, a checklist and a task list is real work. I spent more time reading than typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The volume of artifacts is large.&lt;/strong&gt; Each feature produces a folder with ten files or more. For a one-hour fix, this is too much, and the rule of thumb from the first article applies: use the minimum rigor that removes ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is spec-anchored, not spec-as-source.&lt;/strong&gt; The contract and the tests keep the spec honest, but I did not hand the generation over to the spec alone. A person still decides when the loop stops.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If I have to summarize what the practice added to the theory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The contract-first chain works in practice.&lt;/strong&gt; Constitution, spec, plan and OpenAPI constrain each other, and a hash is enough to prove that a frontend feature left the contract untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clarify step is where the value starts.&lt;/strong&gt; It forces the decisions that nobody wants to take upfront, and it records them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converge finds what the tests did not ask for.&lt;/strong&gt; It does not replace a review, but it makes the gaps visible and turns them into tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost is real.&lt;/strong&gt; More reading, more files, more discipline. It pays off when the rules are real, as in a booking system, and probably not for a script.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project, with all the specs, plans, checklists and tasks, is here: . If you clone it, start from &lt;code&gt;specs/004-bookings-api/&lt;/code&gt; and read the clarifications first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;In the next article I take the challenge I announced: a &lt;strong&gt;port of the backend to another technology&lt;/strong&gt;. The REST contract, the tests, the data model and the Angular application are my fixed points. RoomBook has now a contract with a known hash and a suite of about 110 backend tests, so the question has become measurable: if they stay untouched, the specification survived the move.&lt;/p&gt;

&lt;p&gt;What do you think about Spec Kit on a project with real business rules? Have you tried it, or a different SDD tool? I would love to read your experience in the comments.&lt;/p&gt;

</description>
      <category>github</category>
      <category>speckit</category>
      <category>specdrivendevelopment</category>
      <category>claude</category>
    </item>
    <item>
      <title>Spec-Driven Development with GitHub Spec Kit (Part 1): The Theory</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Mon, 12 Oct 2026 21:20:36 +0000</pubDate>
      <link>https://dev.to/rosidotidev/spec-driven-development-with-github-spec-kit-3d2</link>
      <guid>https://dev.to/rosidotidev/spec-driven-development-with-github-spec-kit-3d2</guid>
      <description>&lt;p&gt;As a software architect and senior software engineer, I find myself in the middle of a change that I can only describe as epochal. The way we think about software development and enterprise architecture is shifting, and so are the roles that humans and "intelligent systems" play today and will play tomorrow. &lt;/p&gt;

&lt;p&gt;My angle is Spec-Driven Development (SDD) with &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt;, seen from where I work every day: software architecture. In this first article I stay on the theory, with a small example of SDD to make it concrete. In the next ones I build a real application with Spec Kit. GitHub Spec Kit is not the only option: alternatives include Kiro, OpenSpec and BMAD. For now, though, GitHub Spec Kit is the one that best fits my preferences and the way I like to work. This is a personal choice, not a claim that one approach is universally better than the others. I wrote this article to share my view and to set a &lt;strong&gt;personal starting point&lt;/strong&gt; that I can come back to (and probably correct) while things evolve. BMAD is one I may explore in a future article, or perhaps a short series of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Spec-Driven Development?
&lt;/h2&gt;

&lt;p&gt;Let's start from the problem. Ask an AI agent to "add user registration to my app" and it has to guess: which fields, which password rules, is the email verified, how are accounts stored? The result is often plausible-looking code resting on dozens of unstated assumptions, many of them wrong. This is what people call &lt;strong&gt;vibe coding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To me, SDD is the answer. It is an approach where a detailed, human- or AI-validated &lt;strong&gt;specification becomes the single source of truth before any code is written&lt;/strong&gt;. Instead of documentation as an afterthought, the process is inverted: the specification is an executable blueprint that drives the AI coding agents.&lt;/p&gt;

&lt;p&gt;Not every SDD approach keeps the spec alive for the same amount of time. I've found a classification featured in numerous articles to be incredibly helpful to distinguish three levels of rigor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec-first&lt;/strong&gt;: a well-thought spec is written before coding to guide the initial implementation. Once the code exists, the spec may be discarded or drift: its value is the initial clarity, which makes it a good fit for AI-assisted first builds and prototypes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-anchored&lt;/strong&gt;: the spec is kept and evolves with the software, through automated tests and contracts. A change in behavior updates both, and tests derived from the spec (BDD scenarios, or OpenAPI with contract testing) fail when they drift. It suits most production systems well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-as-source&lt;/strong&gt;: the spec is the main source file and agents generate or update code directly from its changes. Humans edit only the spec and never the generated code, so drift is eliminated by design. It needs mature, trusted generation tooling and today is practical mainly where generation is well defined, such as server stubs from OpenAPI.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91kwgdmcrsxxfb99krnm.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%2F91kwgdmcrsxxfb99krnm.png" alt=" " width="799" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My rule of thumb: use the minimum level of rigor that removes ambiguity in your context. Spec-first for AI-assisted initial development, spec-anchored for long-lived production systems, spec-as-source only when the generation tooling is mature and trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Specification as a Contract
&lt;/h2&gt;

&lt;p&gt;This is the idea I want at the center of this series.&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%2Fc0jnawbpsr2jambp0x7o.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%2Fc0jnawbpsr2jambp0x7o.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a code-centric flow, requirements documents drift and design diagrams rot, so the code ends up being the de facto truth. Asking "what should this function do?" means reading the implementation. SDD flips the relationship: the specification becomes the authoritative description that humans and machines use to build and maintain the system. The spec declares the intent, the code realizes it.&lt;/p&gt;

&lt;p&gt;I think three properties turn a specification into a &lt;strong&gt;contract&lt;/strong&gt; and not just documentation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Each phase constrains the next.&lt;/strong&gt; Specify answers &lt;em&gt;what&lt;/em&gt;, plan answers &lt;em&gt;how&lt;/em&gt;, then come tasks, implementation and validation. Each phase produces an artifact that guides the following one, creating a chain of accountability from intent to implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec stays the authority.&lt;/strong&gt; When validation finds a gap, you either fix the code or revise the spec. Either way, the specification remains the reference point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is checked, not just read.&lt;/strong&gt; Traditional design documents are read by developers who then write code that hopefully matches them. A contract is meant to be verified: if the code diverges, something should fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Contracts are not new in software. API specifications such as OpenAPI have long played this role: once the API contract is agreed, frontend and backend teams can work in parallel, and any implementation that matches the contract is valid by definition. SDD brings the same logic to the whole build process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model&lt;/strong&gt;: the spec declares the intent, the code is an implementation detail. And I would be careful with false confidence: a passing check only proves that the code matches the spec. If the spec is wrong, the code will faithfully implement the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Spec Kit in a Nutshell
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;Spec Kit&lt;/a&gt; is the open source toolkit from GitHub for spec-driven development. You interact with it through commands in your coding agent, and it plugs into many of them (Copilot, Claude, Gemini, Codex and others). It can also be extended with extensions, presets, workflows and bundles.&lt;/p&gt;

&lt;p&gt;The core process is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specify -&amp;gt; Plan -&amp;gt; Tasks -&amp;gt; Implement -&amp;gt; Converge&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%2Fakspgxb91m9b52v3h73v.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%2Fakspgxb91m9b52v3h73v.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;constitution&lt;/strong&gt;, the project-wide principles and constraints, is defined once per project. Each phase produces a Markdown artifact that feeds the next one. For production features, three optional quality gates are added.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;clarify&lt;/strong&gt;: Asks targeted questions about anything underspecified and folds your answers back into the spec, so you're not planning on top of ambiguity. Run it before planning, optionally with a focus area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;checklist&lt;/strong&gt;: Generates a custom quality checklist These custom checklists are reviewer-owned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;analyze&lt;/strong&gt;: Reports conflicts, gaps, and ambiguities across spec.md, plan.md, and tasks.md&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Simple Example: a Todo List
&lt;/h2&gt;

&lt;p&gt;Theory is easier with a concrete case. Take the simplest idea I can think of, a todo list, and look at what goes where. The example is mine, written just to show the separation. The syntax is the real one from the Spec Kit docs, in GitHub Copilot's default skills mode (&lt;code&gt;/speckit-*&lt;/code&gt;). You type these commands in the chat of your coding agent, not in the terminal. Other agents may invoke them in a different way, so check the docs for your setup. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Constitution&lt;/strong&gt; (project-wide principles, written once):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-constitution Todo List is an adaptive web application.
Every API change starts from the REST contract.
Business logic lives in the backend, never in the UI.
Every feature comes with automated tests.
The UI must work on both phone and desktop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates or updates the project constitution: the guiding principles against which every later phase is evaluated. A few lines from the file look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/.specify/memory/constitution.md

## Core Principles

### I. Adaptive Todo Application
The product MUST be an adaptive web application for managing todo items.
...
### II. REST Contract First
Every API change MUST begin with an update to the REST contract.
...
## Governance
Amendments MUST be reviewed and recorded in the constitution before dependent work proceeds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spec Kit also keeps dependent templates in sync. Create the file once at the beginning of the project, then update it whenever your principles change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Specify&lt;/strong&gt; (what and why, no technology):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-specify Develop a todo list application where a user can add a task,
mark it as done, delete it and filter tasks by status.
A task needs a non-empty title.
Completed tasks stay visible until they are deleted.
Out of scope for now: sharing tasks between users and notifications.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated spec contains four user stories: Add a Task, Mark a Task Done, Delete a Task, and Filter Tasks by Status. Its built-in requirements checklist has 16 items, all checked in this example. A few excerpts from the generated files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/spec.md

## User Scenarios &amp;amp; Testing
### User Story 1 - Add a Task (Priority: P1)
...
## Requirements
- **FR-002**: The system MUST reject an empty or whitespace-only title...
...
## Success Criteria
- **SC-004**: Users can complete the add, complete, delete, and filter journeys on both phone and desktop form factors.

&amp;lt;prj_root&amp;gt;/specs/001-todo-list/checklists/requirements.md

## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spec focuses on the what and why: user-facing behavior, goals, and outcomes, not the tech stack. The built-in checklist reviews the spec's quality and completeness before planning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Plan&lt;/strong&gt; (how, the technology belongs here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-plan Use an Angular web application, a REST API and a backend component (Python)
that holds the business logic, on top of a relational database (SQLite).
Data model: Task(id, title, status, createdAt).
Expose create, read, update and delete endpoints on /tasks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the implementation plan and four supporting design artifacts under &lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/&lt;/code&gt;. The excerpts below are copied from those files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;plan.md&lt;/strong&gt; (Most important file for technical requirements):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/plan.md
## Summary

Deliver an adaptive todo-list web application for adding, completing, filtering, and deleting tasks.
The Angular client uses a contract-first REST API; a Python FastAPI service owns validation and
business rules and persists tasks to a file-backed SQLite database through Python's standard
`sqlite3` module.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;research.md&lt;/strong&gt; (technical discovery file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/research.md
### SQLite from Python

- **Decision**: Use Python 3.12's built-in `sqlite3` module with one connection per request, created
  and closed on the request's worker thread. Keep each mutation in a short transaction and commit
  only after all statements succeed; roll back on failure.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;data-model.md&lt;/strong&gt; (file used for data model):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/data-model.md
## Task

| Field | Type | Required | Rules |
|---|---|---:|---|
| `id` | UUID | Yes | Generated by the backend; unique and immutable. |
| `title` | String | Yes | Must contain at least one non-whitespace character. Valid submitted values are preserved. |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;contracts/openapi.json&lt;/strong&gt; (REST interface spec):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/contracts/openapi.json
  "paths": {
    "/tasks": {
      "get": {
        "operationId": "listTasks",
        "summary": "List tasks, optionally filtered by status",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;quickstart.md&lt;/strong&gt; (setup frontend and backend):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/quickstart.md
## Start the Backend

From the repository root in PowerShell:

```powershell
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e "backend[test]"
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Tasks&lt;/strong&gt; (break the work down):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No arguments here: the command reads the design artifacts and creates &lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/tasks.md&lt;/code&gt;. This file contains 40 tasks: Setup (3), Foundational (8), Add a Task (7), Mark a Task Done (6), Delete a Task (6), Filter Tasks by Status (7), and Polish (3). Each story has a goal, an independent test, and its own test and implementation tasks. In this example, the tests are included because the constitution requires automated tests for every feature.&lt;/p&gt;

&lt;p&gt;The file defines the task format and traceability labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prj_root&amp;gt;/specs/001-todo-list/tasks.md
## Format: `[ID] [P?] [Story] Description`
- **[P]** marks work that touches separate files and has no dependency on an incomplete task.
- **[Story]** identifies the user story for story-phase tasks; setup, foundation, and polish tasks have no story label.
- Every task names the exact file or files to create or modify.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is its User Story 1 section. The &lt;code&gt;[X]&lt;/code&gt; markers shown are the current state, after &lt;code&gt;/speckit-implement&lt;/code&gt; completed the tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Phase 3: User Story 1 - Add a Task (Priority: P1, MVP)
**Goal**: Create a task from a non-empty title and show it in the All task list.
**Independent Test**: Submit a valid title and confirm exactly one active task with the submitted title is returned and displayed; empty and whitespace-only submissions create no task and show a title-required message.
### Tests for User Story 1
- [X] T012 [P] [US1] Add POST `/tasks` and default GET `/tasks` request/response contract tests, including 201, Task response fields, and 422 Problem Details, in `backend/tests/contract/test_create_task_contract.py`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parallel work and phase dependencies are also recorded in the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### Parallel Opportunities
- Setup tasks T001-T003 touch separate backend, frontend, and root configuration files.
- Within each story, its three test tasks use separate contract, integration, and UI test files and can be authored in parallel before story implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before implementation, &lt;code&gt;/speckit-analyze&lt;/code&gt; can check the consistency of spec, plan, and tasks; it is read-only and reports conflicts and gaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Implement&lt;/strong&gt; (build it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-implement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the command that writes the code. It executes the tasks in &lt;code&gt;tasks.md&lt;/code&gt;, running each phase in dependency order and respecting the parallel markers. Before starting, it reads the state of the checklists as a gate: if some items are unchecked it asks before proceeding, and it never changes the checklist markers. For a small feature like our todo list you can run it once and build everything. For a larger one, you can scope each run with an argument (for example only the Setup and Foundational phases), validate the result and then continue.&lt;/p&gt;

&lt;p&gt;If you catch yourself writing "use PostgreSQL" in the specification, move it to the plan. This is the whole point: the spec says &lt;em&gt;what&lt;/em&gt;, the plan says &lt;em&gt;how&lt;/em&gt;. And it is exactly what makes me curious about changing only the plan later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Converge&lt;/strong&gt; (check that nothing was missed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/speckit-converge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assesses the codebase against the spec, the plan and the tasks to confirm that nothing was missed. It is append-only: it never edits or deletes code, and its only possible write is adding tasks to tasks.md. You run it only after implement has run on the current tasks.md. It first prints a summary of the findings graded by severity, then ends in one of two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Converged&lt;/strong&gt;: no gaps found. tasks.md stays unchanged and you can move on to the review or the pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks appended&lt;/strong&gt;: gaps found. The new tasks are added under a Convergence section of tasks.md, you run implement again to complete them and then converge once more, until it reports converged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Role of the Human
&lt;/h2&gt;

&lt;p&gt;SDD does not take people out of the loop: I think it moves the place where their judgment is applied. Accepting generated changes with minimal resistance is just vibe coding with more files. Machines take over the mechanical work: generating code, keeping artifacts consistent, detecting drift. Humans keep authority over intent, domain meaning, risk tolerance and the legal and ethical constraints that no execution trace can reveal. Those constraints have to be written explicitly into the specification, which is where human judgment is encoded and then enforced.&lt;/p&gt;

&lt;p&gt;In practice, for me this means &lt;strong&gt;explicit approval boundaries&lt;/strong&gt;: a breaking schema change, a policy shift, a refactoring proposed by the AI or a compatibility downgrade should never pass without a person's decision. Spec Kit points in the same direction. Every phase produces an artifact you review before moving on, and in its own agentic SDLC tests and scans stay deterministic automation while merge decisions stay with the maintainers.&lt;/p&gt;

&lt;p&gt;For the engineer, I believe, this changes where value comes from: no longer the speed of writing lines of code, but the quality of the specifications they refine and validate, as an orchestrator and validator who defines the &lt;strong&gt;what&lt;/strong&gt; upstream and approves the &lt;strong&gt;how&lt;/strong&gt; at the boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model&lt;/strong&gt;: bounded autonomy, not full automation. Enforcement is automated, meaning stays human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If I have to summarize my starting point in a few lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The specification is a contract.&lt;/strong&gt; Each phase constrains the next, the spec stays the authority, and it is checked and not just read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Spec Kit gives you the workflow:&lt;/strong&gt; Specify -&amp;gt; Plan -&amp;gt; Tasks -&amp;gt; Implement -&amp;gt; Converge, with clarify, checklist and analyze as quality gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans move upstream, and their responsibility grows.&lt;/strong&gt; People decide specifications, guidelines and architectures: engineers and architects evolve from writing code to owning what the system must do and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a reality check.&lt;/strong&gt; Less context drift, fewer hallucinations and no technical debt from chaotic prompting are the declared goals of SDD, not guaranteed results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the upcoming articles, we move from theory to practice: an adaptive Angular web application communicating via REST with a backend component holding the business logic on top of a relational database, fully specified and built using Spec Kit.&lt;br&gt;
Next, I challenge myself by porting the backend to a different technology stack. The core idea here is to primarily adapt the plan, which dictates the technical specifications and stack changes. The REST contract, tests, data model, and Angular application act as our fixed anchor points: if they remain untouched, we prove just how powerfully specification-driven planning can handle technology shifts. The code will be on my GitHub (rosidotidev)&lt;/p&gt;

&lt;p&gt;What has your experience with Spec-Driven Development been like? I'd love to hear your thoughts, questions, or feedback in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>specdrivendevelopment</category>
      <category>sdd</category>
    </item>
    <item>
      <title>Github copilota spec</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 06:42:49 +0000</pubDate>
      <link>https://dev.to/rosidotidev/github-copilota-spec-4n25</link>
      <guid>https://dev.to/rosidotidev/github-copilota-spec-4n25</guid>
      <description></description>
    </item>
    <item>
      <title>Karpathy's LLM Wiki? No Code with Claude or Github Copilot!</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sat, 23 May 2026 15:22:49 +0000</pubDate>
      <link>https://dev.to/rosidotidev/karpathys-llm-wiki-no-code-with-claude-or-github-copilot-5fb0</link>
      <guid>https://dev.to/rosidotidev/karpathys-llm-wiki-no-code-with-claude-or-github-copilot-5fb0</guid>
      <description>&lt;p&gt;I first encountered Andrej Karpathy's &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki gist&lt;/a&gt; and realized it captures something essential about how we should manage knowledge in the AI era: a structured, human-curated knowledge base that grows from what you feed it, not from what an LLM hallucinates. The problem is that most implementations I found required heavy coding. I decided to build a &lt;strong&gt;completely no-code version&lt;/strong&gt; that works seamlessly with both Claude Code and GitHub Copilot's &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; system.&lt;/p&gt;

&lt;p&gt;The result is &lt;code&gt;llm-wiki-nocode&lt;/code&gt;: a production-ready knowledge management system where you ingest documents, ask questions, and maintain a living wiki, all through natural language commands, zero Python required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an LLM Wiki?
&lt;/h2&gt;

&lt;p&gt;Before diving into the implementation, let me explain the concept. An LLM Wiki is a &lt;strong&gt;structured knowledge base&lt;/strong&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingests raw documents&lt;/strong&gt; and extracts entities (tools, people, companies), concepts (patterns, ideas), and relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieves facts&lt;/strong&gt; exclusively from stored knowledge (no training data, no hallucinations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintains integrity&lt;/strong&gt; through automated health checks and human review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compounds knowledge&lt;/strong&gt; as you validate and refine auto-generated answers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key principle is simple: &lt;strong&gt;everything the wiki knows comes from what you feed it.&lt;/strong&gt; Not from the LLM's training data. Not from the web. Only from your sources.&lt;/p&gt;

&lt;p&gt;This solves a real problem. When you ask an LLM a question about your proprietary system, it often confidently makes things up. A wiki-based approach forces it to admit "I found nothing" if the knowledge isn't there. No pollution. No false authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why No-Code?
&lt;/h2&gt;

&lt;p&gt;Most LLM Wiki implementations assume you have a Python environment, dependency management, vector databases, and patience for debugging integrations. That's the wrong barrier to entry.&lt;/p&gt;

&lt;p&gt;I'm also building a &lt;strong&gt;production Python version&lt;/strong&gt; using the &lt;strong&gt;Microsoft Agent Framework&lt;/strong&gt;, structured around agentic workflows, knowledge graph construction, and semantic retrieval. But that's a separate project with its own architecture and tradeoffs. It will be the subject of a future article.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This&lt;/em&gt; project takes the opposite approach: &lt;strong&gt;zero dependencies, zero installation, pure natural language&lt;/strong&gt;. No friction. Immediate usability.&lt;/p&gt;

&lt;p&gt;What if you could ingest a document, ask questions, and maintain your knowledge base entirely through natural language? No terminals. No &lt;code&gt;pip install&lt;/code&gt;. No YAML configuration. Just commands that read like English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wiki-ingest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wiki-query What are the key architectural patterns?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wiki-lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what &lt;code&gt;llm-wiki-nocode&lt;/code&gt; delivers. The system is designed to be &lt;strong&gt;human-operable&lt;/strong&gt;: someone who has never written a line of code can build and maintain their own knowledge base.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Core Operations
&lt;/h2&gt;

&lt;p&gt;The wiki's entire interface consists of three commands. Everything else follows from these three:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;/wiki-ingest&lt;/code&gt;, Build Your Knowledge Base
&lt;/h3&gt;

&lt;p&gt;Ingest is where documents become knowledge. You point it at a raw file (Markdown, plain text, whatever), and it extracts &lt;strong&gt;entities&lt;/strong&gt;, the named things like tools, frameworks, companies, people, and &lt;strong&gt;concepts&lt;/strong&gt;, the abstract ideas like patterns, methodologies, principles. It detects relationships between them, merges new information with existing wiki pages, flags contradictions for your review, and updates the index and audit log in one pass.&lt;/p&gt;

&lt;p&gt;You can pass a file path, use &lt;code&gt;scan&lt;/code&gt; for auto-ingest, or &lt;code&gt;--RESET-ALL&lt;/code&gt; if you want to start fresh. The output is new and updated pages in &lt;code&gt;wiki/&lt;/code&gt;, plus a detailed log of what changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;/wiki-query&lt;/code&gt;, Ask Questions Without Hallucination
&lt;/h3&gt;

&lt;p&gt;Query is the retrieval side. You ask a question in natural language, and the system reads 3–8 relevant wiki pages and synthesizes an answer &lt;strong&gt;exclusively from that content&lt;/strong&gt;. No external knowledge. No training data pollution. This is the core promise: every claim in the answer is cited with a wikilink showing exactly where it came from. The answer is saved to &lt;code&gt;questions_pending/&lt;/code&gt; for your review before it gets integrated as a synthesis page in the wiki.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;/wiki-lint&lt;/code&gt;, Maintain Health
&lt;/h3&gt;

&lt;p&gt;Lint is your wiki's health check. It runs in two phases: first, a deterministic pass that catches broken links, orphaned pages, missing frontmatter, and empty sections. Then a semantic phase that looks for contradictions, stale claims, missing pages, and unreferenced entities. The output is a detailed report with suggested fixes, saved to &lt;code&gt;lint_pending/&lt;/code&gt; for you to review and approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Directory Structure
&lt;/h2&gt;

&lt;p&gt;Understanding the layout is key to understanding how the wiki works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wiki/                      ← Living knowledge base
├── index.md               ← Catalog of all pages
├── log.md                 ← Operation audit trail
├── sources/               ← One page per ingested document
├── entities/              ← Named things: tools, frameworks, companies
├── concepts/              ← Abstract ideas: patterns, methodologies
└── synthesis/             ← Approved answers from your questions

raw/                       ← Documents awaiting ingestion

questions_pending/         ← Auto-generated answers (your review)
questions_approved/        ← Answers you've validated

lint_pending/              ← Wiki health reports (your review)
lint_approved/             ← Fixes you've approved

docs/                      ← Specifications and guides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;code&gt;wiki/&lt;/code&gt; is the single source of truth. Everything else is a &lt;strong&gt;queue&lt;/strong&gt; waiting for a human decision. &lt;code&gt;raw/&lt;/code&gt; is input. &lt;code&gt;questions_pending/&lt;/code&gt; and &lt;code&gt;lint_pending/&lt;/code&gt; are decisions waiting to be made. Once you approve something, it moves to &lt;code&gt;_approved/&lt;/code&gt; and gets integrated back into &lt;code&gt;wiki/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Page Types
&lt;/h3&gt;

&lt;p&gt;The wiki uses three distinct page types to keep knowledge organized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt; are created when you ingest a document, one page per source file. Each source page contains the title, author, date, raw content, and a list of all entities and concepts extracted from that document. This is your audit trail: you know exactly which document something came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entities&lt;/strong&gt; are named, identifiable things. A tool like Jira or Kubernetes. A framework like React or MAF. A company. A person. Each entity page lists its definition, where it appears (which sources and concepts reference it), and related entities. This creates a web of interconnected knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concepts&lt;/strong&gt; are abstract ideas and patterns. A methodology like microservices or CQRS. A principle like DRY or YAGNI. A design pattern. Each concept page includes an explanation, examples (always with source citations), and connections to related concepts and entities. Concepts are how you see patterns across your sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: The Human-Gated Loop
&lt;/h2&gt;

&lt;p&gt;This is the part that makes &lt;code&gt;llm-wiki-nocode&lt;/code&gt; different from other implementations. The system never makes 100 auto-decisions and leaves you to inherit the consequences. Instead, it follows a human-gated approval loop.&lt;/p&gt;

&lt;p&gt;The system generates, ingests a document, answers a question, finds health issues. Then you review what was generated, looking at the &lt;code&gt;_pending/&lt;/code&gt; folders. You decide which auto-generated content is good enough to integrate, and which needs refinement or rejection. Once you approve, the content moves to &lt;code&gt;_approved/&lt;/code&gt; and gets integrated back into the living wiki.&lt;/p&gt;

&lt;p&gt;This loop is non-negotiable. The system assists; you decide. You never wake up one morning and discover the wiki made a thousand bad decisions overnight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Automated generation    →    Human review    →    Integration
         ↓                        ↓                      ↓
   @wiki-querier          questions_pending/      wiki/synthesis/
   @wiki-linter           lint_pending/           wiki pages updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started: A Practical Example
&lt;/h2&gt;

&lt;p&gt;The repo includes three ready-made documents in the &lt;code&gt;bak/&lt;/code&gt; directory for testing. There's &lt;strong&gt;afw-instructions.md&lt;/strong&gt;, technical documentation about Microsoft Agent Framework best practices. &lt;strong&gt;mfa_agents_howto.md&lt;/strong&gt; is a how-to guide for building agents. And &lt;strong&gt;ARCH_BOOKING_PLATFORM.md&lt;/strong&gt; is a simulated system architecture document. These examples show how the wiki handles diverse source material.&lt;/p&gt;

&lt;p&gt;To test the wiki, copy one of these files to &lt;code&gt;raw/&lt;/code&gt; and run ingest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;bak/ARCH_BOOKING_PLATFORM.md raw/
/wiki-ingest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ingestor will parse the architecture doc, extract entities like services, technologies, and teams, pull out concepts like "microservices", "event-driven", and "CQRS", create pages for each in &lt;code&gt;wiki/entities/&lt;/code&gt; and &lt;code&gt;wiki/concepts/&lt;/code&gt;, and log every change in &lt;code&gt;wiki/log.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, ask it a question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wiki-query What are the core services in the booking platform?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The querier will search the wiki for relevant pages, answer using only what's there, cite sources with wikilinks, and save the answer for your 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.amazonaws.com%2Fuploads%2Farticles%2F1p69gnc4zb4essg0w9jt.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%2F1p69gnc4zb4essg0w9jt.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, run a health check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wiki-lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The linter will check for broken links, orphaned pages, contradictions, and suggest fixes. You review the report, decide which fixes to apply, and the wiki improves itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Configuration Layer: &lt;code&gt;.claude/&lt;/code&gt; and &lt;code&gt;.github/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is a critical design decision that deserves explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.claude/&lt;/code&gt; Directory, Claude Code Configuration
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;.claude/&lt;/code&gt; folder contains the complete configuration for Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/
├── CLAUDE.md                  ← Project instructions and behavioral constraints
├── settings.json              ← Command registration and harness configuration
└── commands/
    ├── wiki-ingest.md         ← /wiki-ingest command workflow definition
    ├── wiki-query.md          ← /wiki-query command workflow definition
    └── wiki-lint.md           ← /wiki-lint command workflow definition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: &lt;code&gt;CLAUDE.md&lt;/code&gt; is the main system prompt that defines the wiki's identity and available commands. &lt;code&gt;settings.json&lt;/code&gt; registers each command and maps it to a workflow. Each file in &lt;code&gt;commands/&lt;/code&gt; is a &lt;strong&gt;complete specification&lt;/strong&gt;, it tells Claude exactly how to perform that operation: what to read, what to extract, what format to use, what to validate.&lt;/p&gt;

&lt;p&gt;When you type &lt;code&gt;/wiki-ingest&lt;/code&gt; in Claude Code, it reads &lt;code&gt;commands/wiki-ingest.md&lt;/code&gt; and executes that workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.github/&lt;/code&gt; Directory, GitHub Copilot Configuration
&lt;/h3&gt;

&lt;p&gt;Similarly, &lt;code&gt;.github/&lt;/code&gt; contains GitHub Copilot-specific instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/
├── prompts/
│   ├── wiki-ingest.prompt.md   ← LLM prompt for ingest command
│   ├── wiki-query.prompt.md    ← LLM prompt for query command
│   └── wiki-lint.prompt.md     ← LLM prompt for lint command
├── instructions/
│   └── wiki-schema.instructions.md  ← Shared page format and schema documentation
└── agents/
    ├── wiki-ingestor.agent.md  ← Ingestor agent definition
    ├── wiki-querier.agent.md   ← Querier agent definition
    └── wiki-linter.agent.md    ← Linter agent definition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Copilot reads these files automatically when you open the repo. The &lt;code&gt;prompts/&lt;/code&gt; folder contains the LLM instructions (analogous to Claude's &lt;code&gt;commands/&lt;/code&gt;). The &lt;code&gt;agents/&lt;/code&gt; folder defines how each agent behaves. The &lt;code&gt;instructions/&lt;/code&gt; folder holds shared schema documentation so both Claude Code and Copilot understand the page format identically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model&lt;/strong&gt;: Think of &lt;code&gt;.claude/&lt;/code&gt; and &lt;code&gt;.github/&lt;/code&gt; as &lt;strong&gt;platform-specific adapters&lt;/strong&gt;. The core logic (what to extract, how to format, what to validate) is shared through &lt;code&gt;wiki-schema.instructions.md&lt;/code&gt;. But the way you invoke commands, the syntax, and how each LLM system structures its workflows differs between Claude Code and GitHub Copilot.&lt;/p&gt;

&lt;p&gt;This dual-support approach means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you use Claude Code&lt;/strong&gt;: Open the project, type &lt;code&gt;/wiki-ingest&lt;/code&gt;, Claude reads &lt;code&gt;.claude/commands/wiki-ingest.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you use GitHub Copilot&lt;/strong&gt;: The repo integrates automatically, Copilot reads &lt;code&gt;.github/prompts/wiki-ingest.prompt.md&lt;/code&gt; and uses &lt;code&gt;.github/agents/wiki-ingestor.agent.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you use neither&lt;/strong&gt;: You can still read all the files and understand exactly what each operation does&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No core logic is duplicated. Both versions implement the same three operations with identical semantics, they just speak different dialects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Choices
&lt;/h2&gt;

&lt;p&gt;Working through the full implementation revealed several important patterns worth sharing.&lt;/p&gt;

&lt;p&gt;The wiki uses &lt;strong&gt;retrieval-only queries, not fine-tuning&lt;/strong&gt;. It doesn't train or modify the LLM. Instead, it uses semantic search to find relevant pages and asks the LLM to synthesize an answer from only those pages. This is both simpler to implement and more reliable than RAG with fine-tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entities and concepts are fundamentally different&lt;/strong&gt;, and treating them that way matters. A tool like Jira or PostgreSQL is an entity, a concrete thing you can point to. A pattern like CQRS or event sourcing is a concept, an idea that can apply in many contexts. When the wiki understands this distinction, you can ask "What concepts relate to Kubernetes?" and get a meaningful, connected answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human review before integration&lt;/strong&gt; is non-negotiable. A single document ingest can generate hundreds of pages. If you didn't review them, you'd have garbage in your wiki. The &lt;code&gt;_pending/&lt;/code&gt; folders force a decision point. It's cheap human attention spent upfront that saves you from debugging corrupted data later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit trail is mandatory.&lt;/strong&gt; Every operation, every ingest, query, lint, is logged with timestamps, the files it touched, and what changed. This is how you answer "When did I ingest that architecture doc?" and "What exactly did that ingest change?" questions months later when your memory is fuzzy.&lt;/p&gt;

&lt;p&gt;Finally, the wiki uses &lt;strong&gt;plain Markdown with simple YAML frontmatter&lt;/strong&gt;. No proprietary format. No database. Everything is human-readable, editable, and version-controlled. You can throw it in Git, diff it, and understand what changed without needing special tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Not Included (Yet)
&lt;/h2&gt;

&lt;p&gt;This is a no-code system for knowledge ingestion and retrieval. It doesn't include vector databases or semantic search, you don't need them at small scale, and simple keyword plus fuzzy matching works fine for document lookup. If you scale large, that's a future architectural decision you can make then.&lt;/p&gt;

&lt;p&gt;The wiki doesn't fine-tune or train the model. It works with any LLM that supports Claude Code or Copilot integrations, so you're not locked into a single backend.&lt;/p&gt;

&lt;p&gt;Web ingestion isn't built in. You ingest documents from files. If you want to ingest web pages, you'd scrape them to Markdown first and place them in &lt;code&gt;raw/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Multi-user collaboration is deferred. The system assumes a single human curator making review and approval decisions. Role-based approval and conflict resolution for teams are possible, but not yet implemented.&lt;/p&gt;

&lt;p&gt;None of these are impossible, they're just outside the current scope, waiting for a real use case to drive them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Building &lt;code&gt;llm-wiki-nocode&lt;/code&gt; clarified something important: &lt;strong&gt;the most powerful knowledge systems are simple systems that humans control&lt;/strong&gt;. The wiki doesn't make decisions. It generates, you review, you decide.&lt;/p&gt;

&lt;p&gt;If you're drowning in documentation that lives in Slack threads, scattered Markdown files, or worse, in everyone's heads, this pattern offers a way out. Feed documents to the wiki. Ask questions. Maintain a single source of truth. All without leaving your editor.&lt;/p&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/rosidotidev/llm-wiki-nocode" rel="noopener noreferrer"&gt;GitHub (rosidotidev/llm-wiki-nocode)&lt;/a&gt;. The specifications are in &lt;code&gt;docs/&lt;/code&gt;. Start with a simple document, ingest it, ask a question, and see what happens.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What would you use a no-code LLM wiki for?&lt;/strong&gt; Is knowledge management the bottleneck in your projects, or is something else? I'd love to hear your thoughts in the comments.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>githucopilot</category>
      <category>llmwiki</category>
      <category>ai</category>
    </item>
    <item>
      <title>Microsoft Agent Framework: From Agents to Workflows, Five Iterations of a DOCX-to-Markdown Pipeline</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 26 Apr 2026 12:35:17 +0000</pubDate>
      <link>https://dev.to/rosidotidev/microsoft-agent-framework-from-agents-to-workflows-five-iterations-of-a-docx-to-markdown-pipeline-1l8p</link>
      <guid>https://dev.to/rosidotidev/microsoft-agent-framework-from-agents-to-workflows-five-iterations-of-a-docx-to-markdown-pipeline-1l8p</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/rosidotidev/microsoft-agent-framework-from-zero-to-multi-agent-pipeline-1np2"&gt;previous article&lt;/a&gt;, I built a multi-agent pipeline that reads a Markdown backlog and provisions Epics and Stories on Jira. That project used two agents collaborating sequentially, wired together with plain Python orchestration.&lt;/p&gt;

&lt;p&gt;This time I wanted to understand something specific: does the Microsoft Agent Framework (MFA) have a construct for defining execution graphs, similar to what LangGraph offers with its &lt;code&gt;StateGraph&lt;/code&gt; or CrewAI with its &lt;code&gt;Flow&lt;/code&gt;? In other words, can you define nodes, edges, and let the framework handle execution order, state propagation, and event streaming? To find out, I needed a use case that was practical enough to be reusable, complex enough to stress the graph model, and different enough from the Jira pipeline to force me into new territory.&lt;/p&gt;

&lt;p&gt;I landed on &lt;strong&gt;converting Word documents into rich Markdown files with AI-generated image descriptions&lt;/strong&gt;. The kind of structured, image-heavy documents (functional specifications, architecture documents, technical guides) that are notoriously hard to index in a RAG system because the images carry as much information as the text. A useful asset in itself, and a good bench for a multi-step pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  MFA Workflows in 30 Seconds
&lt;/h2&gt;

&lt;p&gt;The answer to my question turned out to be yes. The framework provides a &lt;a href="https://learn.microsoft.com/en-us/agent-framework/workflows/" rel="noopener noreferrer"&gt;Workflow system&lt;/a&gt; built on three primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Executor&lt;/strong&gt;: a processing node. You subclass it, implement a &lt;code&gt;@handler&lt;/code&gt; method, and it receives typed input and produces output. An executor can wrap an agent, call an API directly, or run pure Python, the framework doesn't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WorkflowBuilder&lt;/strong&gt;: the graph constructor. You declare a start node, then chain &lt;code&gt;.add_edge(source, target)&lt;/code&gt; calls to define the execution path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edges&lt;/strong&gt;: directed connections between executors. Data flows along edges via &lt;code&gt;ctx.send_message()&lt;/code&gt;, while &lt;code&gt;ctx.set_state()&lt;/code&gt; / &lt;code&gt;ctx.get_state()&lt;/code&gt; provide a shared store accessible by any node in the graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you call &lt;code&gt;workflow.run(input, stream=True)&lt;/code&gt;, the framework walks the graph and emits events (&lt;code&gt;executor_invoked&lt;/code&gt;, &lt;code&gt;executor_completed&lt;/code&gt;, &lt;code&gt;output&lt;/code&gt;) that give you real-time visibility into each step.&lt;/p&gt;

&lt;p&gt;With these primitives in hand, I could start building.&lt;/p&gt;

&lt;p&gt;The idea behind the use case is simple: if you can turn every image into a detailed textual description embedded directly in the Markdown, a future RAG pipeline can "see" diagrams, screenshots, and flowcharts without needing a multimodal model at query time. The image content becomes searchable text.&lt;/p&gt;

&lt;p&gt;What started as a straightforward agent-based pipeline evolved through five iterations into something quite different: a &lt;strong&gt;workflow-based, LLM-minimal architecture&lt;/strong&gt; where Python does 90% of the work and the LLM is called only for what it's genuinely good at, describing images.&lt;/p&gt;

&lt;p&gt;All the code is available on &lt;a href="https://github.com/rosidotidev/MSFTAgentSample" rel="noopener noreferrer"&gt;GitHub (MSFTAgentSample)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Use Case
&lt;/h2&gt;

&lt;p&gt;Given a folder of &lt;code&gt;.docx&lt;/code&gt; files, the pipeline should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract&lt;/strong&gt; all text, tables, and images from each document, preserving the logical order and tracking where each image appears in the text flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Describe&lt;/strong&gt; every extracted image using GPT-4o vision, producing thorough, structured descriptions that capture every visible element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assemble&lt;/strong&gt; a clean Markdown document where headings, paragraphs, tables, and images are rendered correctly, with each image followed by its AI-generated description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; the final &lt;code&gt;.md&lt;/code&gt; files to disk, ready for indexing or publishing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 6. Functional Requirements&lt;/span&gt;

The system shall provide a task management interface...

&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./images/img-003-a1b2c3d4.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gs"&gt;**[Image Description]:**&lt;/span&gt; This is a screenshot of a UI mockup showing
a task management dashboard with three columns: To Do, In Progress,
and Done. Each column contains card-like elements with task titles...
&lt;span class="gs"&gt;**[End Image Description]**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Starting Point: Agents Only (v1)
&lt;/h2&gt;

&lt;p&gt;The first version followed the same pattern as the Jira pipeline: sequential agents, no workflow. Three agents collaborating in a chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DocExtractorAgent → ImageAnalystAgent → ContentAssemblerAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent wraps a specific capability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DocExtractorAgent&lt;/strong&gt; calls a &lt;code&gt;@tool&lt;/code&gt; function that uses &lt;code&gt;unstructured&lt;/code&gt; and &lt;code&gt;python-docx&lt;/code&gt; to extract text segments and images from &lt;code&gt;.docx&lt;/code&gt; files. The output is a structured JSON with &lt;code&gt;text_segments&lt;/code&gt; (typed: Title, NarrativeText, Table, ListItem, image) and an &lt;code&gt;images&lt;/code&gt; array with file paths and surrounding context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageAnalystAgent&lt;/strong&gt; receives the extraction JSON, collects all image references, and calls a &lt;code&gt;describe_images&lt;/code&gt; tool that sends each image to GPT-4o-mini vision. The vision prompt is thorough: it asks for image type identification, element-by-element description, all visible text transcription, relationships and flow, and layout details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ContentAssemblerAgent&lt;/strong&gt; takes the extraction data plus the image descriptions and produces the final Markdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The orchestration is plain Python, same as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;extractor_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input_dir=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;INPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, output_dir=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;analyst_agent&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="n"&gt;response1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;assembler_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extraction:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;descriptions:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;output_dir=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked, but several problems were immediately visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke and Why
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The LLM forgot to close tags.&lt;/strong&gt; The ContentAssemblerAgent was instructed to wrap each image description in &lt;code&gt;**[Image Description]:**&lt;/code&gt; and &lt;code&gt;**[End Image Description]**&lt;/code&gt; tags. But on long documents, the LLM would sometimes omit the closing tag, breaking downstream parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No observability.&lt;/strong&gt; With plain &lt;code&gt;agent.run()&lt;/code&gt; calls, you only see the final output. You don't know which agent is running, how long each step took, or where a failure occurred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential image analysis was slow.&lt;/strong&gt; Each image was sent to GPT-4o-mini one at a time. A document with 10 images meant 10 sequential API calls, each taking 3-5 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalability was questionable.&lt;/strong&gt; The ContentAssemblerAgent received the entire extraction JSON in its prompt. For a 50-page document, that JSON could exceed the model's context window, causing truncation or quality degradation.&lt;/p&gt;

&lt;p&gt;These issues led me to explore the &lt;strong&gt;Workflow&lt;/strong&gt; feature of the Microsoft Agent Framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Workflows (v2)
&lt;/h2&gt;

&lt;p&gt;With the workflow primitives I described above, the first step was straightforward: wrap each agent inside an executor. Here's what a minimal executor looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_executor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@handler&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;do_something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# forward to next executor
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire them up, and you get a graph with event streaming out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;WorkflowBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_executor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;executor_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executor_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executor_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workflow&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="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor_invoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor_completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Done: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Result: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For v2, the graph was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DocExtractorExecutor ──▶ ImageAnalystExecutor ──▶ ContentAssemblerExecutor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing worth noting: besides edge-based data flow (&lt;code&gt;ctx.send_message&lt;/code&gt;), executors can also use &lt;strong&gt;shared state&lt;/strong&gt; (&lt;code&gt;ctx.set_state&lt;/code&gt; / &lt;code&gt;ctx.get_state&lt;/code&gt;). This lets a node store data that any downstream node can read directly, without it having to pass through intermediate nodes.&lt;/p&gt;

&lt;p&gt;For example, the DocExtractor saves the extraction result to state so that the ContentAssembler (two steps later) can read it directly, without the data having to flow through the ImageAnalyst:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocExtractorExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="nd"&gt;@handler&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_agent&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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# shared state
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# edge to next executor
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same functionality as v1, but now with event streaming and a clear graph structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Template + Filler Pattern (v3)
&lt;/h2&gt;

&lt;p&gt;The closing-tag problem from v1 persisted in v2, because the ContentAssembler was still an LLM generating the full Markdown including image descriptions. LLMs are inherently unreliable for repetitive structural tasks like "always close every tag."&lt;/p&gt;

&lt;p&gt;The solution was to split the assembly into two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TemplateAssembler&lt;/strong&gt; (LLM), generates the Markdown structure with &lt;code&gt;&amp;lt;!--IMG:filename--&amp;gt;&lt;/code&gt; HTML comment placeholders instead of actual descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TemplateFiller&lt;/strong&gt; (pure Python), reads the image descriptions from state and replaces each placeholder with the formatted description using a regex.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workflow became a 4-node graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DocExtractor ──▶ ImageAnalystStateful ──▶ TemplateAssembler ──▶ TemplateFiller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TemplateFiller is completely deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_PLACEHOLDER_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!--IMG:([\w.\-]+)--&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desc_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# leave placeholder as-is
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;

&lt;span class="n"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_PLACEHOLDER_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_replace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No LLM, no token cost, no randomness. The closing tag problem disappeared because the Python code always writes both the opening and closing tags.&lt;/p&gt;

&lt;p&gt;This was the turning point. It made me ask: &lt;strong&gt;where else am I using an LLM for something Python can do better?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling Up (v4): Parallel Vision + Chunked Assembly
&lt;/h2&gt;

&lt;p&gt;With the architecture cleaned up, two scaling bottlenecks remained:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image analysis was sequential.&lt;/strong&gt; The ImageAnalyst agent called the &lt;code&gt;describe_images&lt;/code&gt; tool, which internally looped through images one at a time. For N images, that's N sequential API calls.&lt;/p&gt;

&lt;p&gt;The fix: a new &lt;code&gt;ParallelImageAnalystExecutor&lt;/code&gt; that bypasses the agent entirely and calls &lt;code&gt;AsyncOpenAI&lt;/code&gt; directly with &lt;code&gt;asyncio.gather&lt;/code&gt;, bounded by a semaphore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ParallelImageAnalystExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parallel_image_analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@handler&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extraction_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aclient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncOpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_CONCURRENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;_describe_image_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aclient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_images&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;result_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;descriptions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;MAX_CONCURRENT=5&lt;/code&gt; (configurable via environment variable), 10 images now take ~5 seconds instead of ~40.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template assembly could exceed context window.&lt;/strong&gt; The TemplateAssembler received the entire document extraction in a single prompt. For large documents, this meant prompt truncation or degraded output quality.&lt;/p&gt;

&lt;p&gt;The fix: a &lt;code&gt;ChunkedTemplateAssemblerExecutor&lt;/code&gt; that splits the document's &lt;code&gt;text_segments&lt;/code&gt; at heading boundaries (using the &lt;code&gt;Title&lt;/code&gt; type from &lt;code&gt;unstructured&lt;/code&gt;), processes each chunk with a separate LLM call, and concatenates the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_chunk_segments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each LLM call sees only one section (~5-15 paragraphs), keeping token usage low and quality consistent regardless of document length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going LLM-Minimal (v5): The Final Architecture
&lt;/h2&gt;

&lt;p&gt;At this point I stepped back and asked: &lt;strong&gt;do I actually need an LLM for any of the remaining steps?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The DocExtractor
&lt;/h3&gt;

&lt;p&gt;The DocExtractor agent received a prompt, called the &lt;code&gt;extract_docx_files&lt;/code&gt; tool, and returned the tool's JSON output unchanged. The agent was a pure passthrough, it added zero value over calling the Python function directly.&lt;/p&gt;

&lt;p&gt;The replacement is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.tools.docx_extractor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_extract_all_docx&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocExtractorDirectExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Executor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_extractor_direct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;

    &lt;span class="nd"&gt;@handler&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_all_docx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_dir&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_dir&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One LLM call eliminated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The TemplateAssembler
&lt;/h3&gt;

&lt;p&gt;This is where I had to think harder. The LLM was converting &lt;code&gt;text_segments&lt;/code&gt; JSON into Markdown. The segments already had type information:&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="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;"Title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6.1 Functional Overview"&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="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;"NarrativeText"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The system shall provide..."&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="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;"Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"| Col1 | Col2 |..."&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="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;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[IMAGE: img-003-a1b2c3d4.png]"&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="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;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enable a user to capture a task in less than 10 seconds"&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;I investigated what metadata &lt;code&gt;unstructured&lt;/code&gt; provides. It turned out that &lt;code&gt;Title&lt;/code&gt; elements have a &lt;code&gt;category_depth&lt;/code&gt; field (0 for top-level headings, 1 for sub-headings), and heading numbering patterns (&lt;code&gt;6.1&lt;/code&gt;, &lt;code&gt;6.1.2&lt;/code&gt;) can be used to infer deeper nesting.&lt;/p&gt;

&lt;p&gt;The entire Markdown assembly became a &lt;code&gt;for&lt;/code&gt; loop with type-based rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_segments_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;prev_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListItem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListItem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_heading_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ## / ### / #### based on numbering
&lt;/span&gt;            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListItem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# already Markdown from extractor
&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;![Image](./images/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!--IMG:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# NarrativeText, Text, etc.
&lt;/span&gt;            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;prev_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg_type&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deterministic, instantaneous, zero cost. And the heading hierarchy is actually more consistent than what the LLM produced, because it follows strict rules rather than "inferring" structure.&lt;/p&gt;

&lt;p&gt;Many more LLM calls eliminated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Final Workflow Graph
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DocExtractorDirect ──▶ ParallelImageAnalyst ──▶ DeterministicAssembler ──▶ TemplateFillerMulti
   (Python)               (async Vision)             (Python)                  (Python)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;LLM usage: only the N parallel vision calls for image descriptions.&lt;/strong&gt; Everything else is pure Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Workflow Builder
&lt;/h3&gt;

&lt;p&gt;The wiring is clean and concise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WorkflowBuilder&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_doc_ingest_tpl_multi_opt_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DocExtractorDirectExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;analyst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ParallelImageAnalystExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;assembler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DeterministicAssemblerExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;filler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TemplateFillerMultiExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WorkflowBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_executor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;analyst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assembler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assembler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;Notice: &lt;strong&gt;no &lt;code&gt;client&lt;/code&gt; or &lt;code&gt;options&lt;/code&gt; parameter.&lt;/strong&gt; Since no executor uses an agent, there's no LLM client to inject. The &lt;code&gt;ParallelImageAnalyst&lt;/code&gt; creates its own &lt;code&gt;AsyncOpenAI&lt;/code&gt; client internally because it needs vision, not chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Entry Point
&lt;/h3&gt;

&lt;p&gt;The entry point consumes the workflow's event stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_doc_ingest_tpl_multi_opt_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workflow&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input_dir=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;INPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, output_dir=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor_invoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_extractor_direct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📖 Extracting documents (direct)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parallel_image_analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔍 Analysing images (parallel)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deterministic_assembler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📝 Assembling Markdown (deterministic)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_filler_multi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔧 Filling image descriptions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor_completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   ✓ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;assembled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssembledDocumentList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;assembled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   ✅ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Evolution Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Entry Point&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;LLM Calls&lt;/th&gt;
&lt;th&gt;Key Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main_doc_ingest.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sequential agents&lt;/td&gt;
&lt;td&gt;3 per file&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main_doc_ingest_wfl.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workflow (3 executors)&lt;/td&gt;
&lt;td&gt;3 per file&lt;/td&gt;
&lt;td&gt;Graph-based, event streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main_doc_ingest_wfl_tpl.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workflow (4 executors)&lt;/td&gt;
&lt;td&gt;2 + N images&lt;/td&gt;
&lt;td&gt;Template + filler pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main_doc_ingest_wfl_tpl_multi.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workflow (4 executors)&lt;/td&gt;
&lt;td&gt;N parallel + M chunks&lt;/td&gt;
&lt;td&gt;Parallel vision, chunked assembly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main_doc_ingest_wfl_tpl_multi_opt.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workflow (4 executors)&lt;/td&gt;
&lt;td&gt;N parallel only&lt;/td&gt;
&lt;td&gt;LLM-minimal: Python does the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cost reduction from v1 to v5 is dramatic. For a document with 5 images and 10 sections, v1 makes ~8 LLM calls (3 agents). V5 makes exactly 5 (one per image, in parallel). The extraction and assembly are free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Executors don't have to wrap agents.&lt;/strong&gt; The framework's &lt;code&gt;Executor&lt;/code&gt; class is a general-purpose processing unit. It can wrap an agent, call an API directly, or run pure Python logic. The best executors are often the ones with no LLM at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The template + filler pattern is widely applicable.&lt;/strong&gt; Whenever you need an LLM to generate structured output with repetitive elements (tags, placeholders, formatting), consider splitting the task: let the LLM generate the structure, and let Python fill in the repetitive parts deterministically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;asyncio.gather&lt;/code&gt; with a semaphore is the right pattern for parallel API calls.&lt;/strong&gt; It's simple, it respects rate limits, and it keeps you in control of concurrency. No need for complex job queues or thread pools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;unstructured&lt;/code&gt; gives you more metadata than you think.&lt;/strong&gt; Before deciding to use an LLM for heading hierarchy, I checked what &lt;code&gt;unstructured&lt;/code&gt; already provides. It turned out it has &lt;code&gt;category_depth&lt;/code&gt;, &lt;code&gt;parent_id&lt;/code&gt;, and typed elements (Title, ListItem, NarrativeText, Table), enough to build a complete Markdown document without any AI involvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow versioning via file suffixes works well.&lt;/strong&gt; Each iteration is a separate file (&lt;code&gt;doc_ingest.py&lt;/code&gt;, &lt;code&gt;doc_ingest_tpl.py&lt;/code&gt;, &lt;code&gt;doc_ingest_tpl_multi.py&lt;/code&gt;, &lt;code&gt;doc_ingest_tpl_multi_opt.py&lt;/code&gt;) with its own entry point. Previous versions are preserved for comparison and rollback. No git archaeology needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Takeaways
&lt;/h2&gt;

&lt;p&gt;There is an old saying: "When all you have is a hammer, everything looks like a nail." Working with LLMs can feel exactly like that. You have this incredibly powerful tool that can understand language, generate text, analyze images, and it becomes tempting to route every step through it. Extraction? Let the LLM handle it. Assembly? The LLM can figure out the Markdown. Heading hierarchy? Surely the LLM knows best.&lt;/p&gt;

&lt;p&gt;The single biggest takeaway from this project is that &lt;strong&gt;the best LLM call is the one you don't make&lt;/strong&gt;. Before building v5, I assumed the extraction and assembly steps "needed" an LLM because they involved understanding document structure. They didn't. The extraction was already a Python tool behind an agent wrapper. The assembly was a deterministic mapping from typed segments to Markdown. The LLM added cost, latency, and randomness without adding value.&lt;/p&gt;

&lt;p&gt;Stripping away those unnecessary calls didn't just save tokens. It made the pipeline faster, more predictable, and easier to debug. When something goes wrong in a pure Python executor, you get a stack trace, not a hallucination.&lt;/p&gt;

&lt;p&gt;The MFA workflow model made this evolution natural. Because each step is an independent executor wired into a graph, I could replace an agent-based node with a pure Python node without touching anything else. The framework doesn't push you toward LLMs, it gives you the scaffolding and lets you decide what goes inside each node. That turned out to be exactly the right level of abstraction.&lt;/p&gt;

&lt;p&gt;The pattern of wrapping agents inside executors and passing data through shared state also proved valuable for reuse. The same agent can be plugged into different workflows without modification, because the executor handles the wiring and the state handles the context. This, combined with a clean, well-defined workflow API, made it straightforward to evolve the solution incrementally, from a naive agent chain all the way to an aggressively optimized pipeline, without ever having to rewrite from scratch.&lt;/p&gt;

&lt;p&gt;If the first article was about discovering the framework's building blocks, this one was about learning when &lt;em&gt;not&lt;/em&gt; to use them. Use the LLM where it genuinely shines, like describing images. For everything else, write Python.&lt;/p&gt;

&lt;p&gt;The generated Markdown files, with their embedded image descriptions, are ready for a RAG pipeline. A simple chunking strategy (split on headings, keep image descriptions with their context) would make both text and image content searchable through standard text embeddings, no multimodal retrieval model required.&lt;/p&gt;

&lt;p&gt;All the code is available on &lt;a href="https://github.com/rosidotidev/MSFTAgentSample" rel="noopener noreferrer"&gt;GitHub (MSFTAgentSample)&lt;/a&gt;. The &lt;a href="https://github.com/rosidotidev/MSFTAgentSample/blob/main/PIPELINE_DOCX_TO_MD.md" rel="noopener noreferrer"&gt;PIPELINE_DOCX_TO_MD.md&lt;/a&gt; file documents all five variants with setup instructions and usage commands.&lt;/p&gt;

</description>
      <category>agentframework</category>
      <category>openai</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Microsoft Agent Framework: From Zero to Multi-Agent Pipeline</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:53:25 +0000</pubDate>
      <link>https://dev.to/rosidotidev/microsoft-agent-framework-from-zero-to-multi-agent-pipeline-1np2</link>
      <guid>https://dev.to/rosidotidev/microsoft-agent-framework-from-zero-to-multi-agent-pipeline-1np2</guid>
      <description>&lt;p&gt;I have some background with other agent frameworks like CrewAI and LangGraph, so when Microsoft released the &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Agent Framework&lt;/a&gt;, a lightweight Python package for building AI agents with native MCP (Model Context Protocol) support, I was curious to give it a try. I decided to build something practical: a pipeline that reads a product backlog from a Markdown file and automatically creates Epics and Stories on Jira. I chose this specific use case because I had already implemented it with CrewAI, so I was familiar with the configuration setup and could focus on comparing the frameworks rather than figuring out the integration details from scratch.&lt;/p&gt;

&lt;p&gt;As reported in the &lt;a href="https://learn.microsoft.com/en-us/agent-framework/overview/?pivots=programming-language-python" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;, the Microsoft Agent Framework is the direct successor of both &lt;strong&gt;Semantic Kernel&lt;/strong&gt; and &lt;strong&gt;AutoGen&lt;/strong&gt;, created by the same Microsoft teams. It combines AutoGen's simple abstractions for single- and multi-agent patterns with Semantic Kernel's enterprise-grade features like session-based state management, type safety, telemetry, and extensive model support. On top of that, the Microsoft Agent Framework introduces workflows for explicit control over multi-agent execution paths and a robust state management system for long-running and human-in-the-loop scenarios.&lt;/p&gt;

&lt;p&gt;What I found was a framework that favors simplicity and explicitness. You write Python functions, you wire them together, and you stay in control of the flow. In this article, I walk through the incremental approach I followed, from an "hello world" agent to a fully modular multi-agent pipeline.&lt;/p&gt;

&lt;p&gt;You can find all the code shown in this post on this &lt;a href="https://github.com/rosidotidev/MSFTAgentSample" rel="noopener noreferrer"&gt;GitHub repo (MSFTAgentSample)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Used So Far
&lt;/h2&gt;

&lt;p&gt;I have only scratched the surface of the framework, but here are the building blocks I worked with in this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Agent&lt;/code&gt;&lt;/strong&gt;: the core class. You give it a name, instructions, a chat client, and a list of tools. It runs autonomously, deciding which tools to call and when to stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OpenAIChatClient&lt;/code&gt;&lt;/strong&gt;: one of the available LLM providers. The framework integrates with most major LLMs, but for simplicity I used OpenAI since I still had some tokens to spend :-).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MCPStdioTool&lt;/code&gt;&lt;/strong&gt;: a bridge to any MCP server. Point it at a command and it auto-discovers all available tools via the MCP protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@tool&lt;/code&gt;&lt;/strong&gt;: a decorator to turn any Python function into a tool the agent can invoke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is certainly more to explore, but these four primitives were enough to build a fully working multi-agent pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Hello World, One Agent, No Tools
&lt;/h2&gt;

&lt;p&gt;The very first thing I did was verify that the framework works. The simplest possible setup: one agent, one LLM client, one hardcoded query, no tools at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatOptions&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_manager_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_CHAT_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OpenAIChatOptions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ManagerAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a manager agent. Answer the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s query &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;as accurately and concisely as possible.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&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="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is a Large Language Model and how does it work?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&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="nf"&gt;run_manager_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mental Model: This is the equivalent of a "print hello world" in the agent framework world. You create a client, create an agent, call &lt;code&gt;agent.run()&lt;/code&gt;, and print the result. Everything is async, so you need &lt;code&gt;asyncio.run()&lt;/code&gt; as the entry point. The &lt;code&gt;.env&lt;/code&gt; file provides the API key and model name via &lt;code&gt;python-dotenv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice how explicit everything is. There is no magic configuration, no auto-discovery. You pass the API key, you choose the model, you write the instructions. The agent's identity is fully defined by a single &lt;code&gt;instructions&lt;/code&gt; string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Adding an MCP Tool (Jira)
&lt;/h2&gt;

&lt;p&gt;Once the basics worked, the next step was connecting the agent to the real world. The Microsoft Agent Framework has first-class support for MCP (Model Context Protocol), which is the standard for exposing tools to AI agents. The &lt;code&gt;mcp-atlassian&lt;/code&gt; package provides a full MCP server for Jira and Confluence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MCPStdioTool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatOptions&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# MCP Proxy: auto-discovers all Jira tools via MCP protocol
&lt;/span&gt;    &lt;span class="n"&gt;jira_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPStdioTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jira_server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipenv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-atlassian&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_USERNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_USERNAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_CHAT_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIChatOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;jira_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JiraManagerAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a professional Project Management Assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You have direct access to Jira via integrated tools. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your goal is to help users manage tickets, track progress, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and create issues.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;jira_proxy&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Jira Manager Agent is online...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;user_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        IMPORTANT: Execute each step below ONE AT A TIME.
        Step 1: Create an epic in the SARI project called &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Shopping List&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.
        Step 2: Create a story: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Story 1: Shopping List CRUD Angular UI&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; 
                and set the epic as parent.
        Step 3: Create a story: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Story 2: Shopping List CRUD Angular 
                in memory mocked service&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; and set the epic as parent.
        Create issues one at a time, never in parallel.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;jira_agent&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="n"&gt;user_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Agent Response:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;jira_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&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="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key piece here is &lt;code&gt;MCPStdioTool&lt;/code&gt;. You point it at a command (&lt;code&gt;pipenv run mcp-atlassian&lt;/code&gt;), pass the necessary environment variables, and the framework auto-discovers every tool the MCP server exposes: &lt;code&gt;jira_create_issue&lt;/code&gt;, &lt;code&gt;jira_search&lt;/code&gt;, &lt;code&gt;jira_get_issue&lt;/code&gt;, &lt;code&gt;jira_link_to_epic&lt;/code&gt;, and many more. The agent sees all of them and decides which ones to call based on your query.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Hard Lesson: Parallel Tool Calls
&lt;/h3&gt;

&lt;p&gt;This step is where I hit my first real problem. When asked to create an epic and two stories, the agent would sometimes send multiple &lt;code&gt;jira_create_issue&lt;/code&gt; calls in parallel. The second call would fail with a cryptic error: &lt;code&gt;expected 'key' property to be a string&lt;/code&gt;. After adding debug logging and investigating, I discovered that the MCP server cannot handle parallel tool calls reliably.&lt;/p&gt;

&lt;p&gt;The fix was surprisingly simple: tell the agent explicitly in its instructions to "Create issues ONE AT A TIME, never in parallel." This is a pattern I now apply consistently. If your MCP server doesn't handle concurrency well, just instruct the agent accordingly. It respects the instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Two-Agent Pipeline (Monolithic)
&lt;/h2&gt;

&lt;p&gt;With the Jira integration working, I wanted to build something more structured: a pipeline with two agents collaborating sequentially. The idea was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;BacklogReaderAgent&lt;/strong&gt; reads a Markdown backlog file from disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JiraExecutorAgent&lt;/strong&gt; takes the backlog content and creates all issues on Jira&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To give agents the ability to read and write files, I used the &lt;code&gt;@tool&lt;/code&gt; decorator to create custom function tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name of the file to read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read and return the contents of a file from the input directory.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The content to write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write content to a timestamped file in the output directory.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y%m%d_%H%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execution_result_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File written: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Annotated[str, "description"]&lt;/code&gt; syntax is how you document parameters for the agent. The framework reads these annotations and exposes them as part of the tool schema, so the LLM knows what to pass.&lt;/p&gt;

&lt;p&gt;Then, the two agents and the orchestration logic, all in one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# Agent 1: reads the backlog file
&lt;/span&gt;    &lt;span class="n"&gt;reader_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BacklogReaderAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a backlog reader assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When asked, use the read_file tool to read a markdown file. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return the full contents of the file as-is.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Agent 2: executes the backlog on Jira
&lt;/span&gt;    &lt;span class="n"&gt;executor_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JiraExecutorAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a Jira execution assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create issues ONE AT A TIME, never in parallel. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;After all operations, write a summary using write_file.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;jira_proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Orchestration: sequential pipeline
&lt;/span&gt;    &lt;span class="n"&gt;read_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reader_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the file &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; and return its contents.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;backlog_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="n"&gt;exec_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;executor_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Execute the following backlog on Jira.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog_content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mental Model: Notice that the orchestration is plain Python. There is no pipeline abstraction, no DAG. You call &lt;code&gt;agent.run()&lt;/code&gt;, get the result, and pass it to the next agent. &lt;strong&gt;You&lt;/strong&gt; are the orchestrator.&lt;/p&gt;

&lt;p&gt;The backlog file is a simple Markdown document placed in the &lt;code&gt;input/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Weather Dashboard Backlog - SARI Project&lt;/span&gt;

&lt;span class="gu"&gt;## Epic: Weather Dashboard&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Type: Epic
&lt;span class="p"&gt;-&lt;/span&gt; Description: Real-time weather dashboard application.

&lt;span class="gu"&gt;### Stories&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Story 1: City Search and Autocomplete**&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Type: Story
&lt;span class="p"&gt;  -&lt;/span&gt; Description: Implement a search bar with autocomplete...
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Story 2: Current Weather Display**&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Type: Story
&lt;span class="p"&gt;  -&lt;/span&gt; Description: Show current weather conditions...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads this, understands the structure, and creates the epic first, then each story linked to the epic as parent. All on Jira, automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Modular Pipeline with Pydantic Validation
&lt;/h2&gt;

&lt;p&gt;The monolithic version worked perfectly, but everything was in one file. For a production-ready layout, I refactored the code into a well-structured directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MSFTAgentSample/
├── afw_core/
│   ├── agents/
│   │   ├── backlog_reader.py
│   │   └── jira_executor.py
│   ├── tools/
│   │   ├── file_reader.py
│   │   └── file_writer.py
│   ├── mcps/
│   │   └── jira.py
│   ├── llms/
│   │   └── openai.py
│   └── models/
│       └── backlog.py
│
├── input/
│   └── backlog.md
├── output/
├── main_backlog_from_md_std.py
└── .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module has a single responsibility and exposes a factory function. For example, the agent definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# afw_core/agents/backlog_reader.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BacklogReaderAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a backlog reader assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When asked, use the read_file tool to read a markdown file. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;After reading, respond with ONLY a JSON object matching this schema: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;epic_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;int&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;story_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;int&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;string&amp;gt;}.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# afw_core/agents/jira_executor.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JiraExecutorAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a Jira execution assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create issues ONE AT A TIME, never in parallel. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When linking stories to an epic, first create the epic, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;then create each story and set the epic as parent. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;After all operations, write a summary using write_file.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&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 convention I adopted is: &lt;strong&gt;name and instructions are hardcoded&lt;/strong&gt; inside the factory function (they are intrinsic to the agent's identity), while &lt;strong&gt;client, options, and tools are always injected&lt;/strong&gt; from outside (they are infrastructure concerns). This separation keeps agent definitions clean and reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pydantic for Structured Output
&lt;/h3&gt;

&lt;p&gt;A key improvement in the modular version was adding Pydantic validation between the two agents. Instead of passing raw text from the reader to the executor, I defined a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# afw_core/models/backlog.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BacklogOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;epic_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;story_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader agent is instructed to return JSON matching this schema. The main script validates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.models.backlog&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BacklogOutput&lt;/span&gt;

&lt;span class="n"&gt;read_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reader_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the file &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; and return its contents.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;backlog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BacklogOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Backlog loaded: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;epic_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; epic(s), &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;story_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; stories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent returns malformed JSON, Pydantic throws a validation error immediately, rather than letting corrupted data propagate to the executor agent. This is a simple but effective pattern for inter-agent data contracts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Entry Point
&lt;/h3&gt;

&lt;p&gt;The modular entry point becomes clean orchestration logic with no implementation details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main_backlog_from_md_std.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.llms.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.mcps.jira&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_proxy&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.tools.file_reader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.tools.file_writer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;write_file&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.agents.backlog_reader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;create_reader_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.agents.jira_executor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;create_executor_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;afw_core.models.backlog&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BacklogOutput&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_CHAT_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jira_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_proxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;reader_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_reader_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;executor_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_executor_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;jira_proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 1: Read and validate
&lt;/span&gt;    &lt;span class="n"&gt;read_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reader_agent&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the file &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;backlog.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; and return its contents.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;backlog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BacklogOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Execute on Jira
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exec_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;executor_agent&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Execute the following backlog on Jira.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;jira_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&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="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How you can see, the entry point reads like a recipe: create the infrastructure, create the agents, run them in sequence, handle cleanup. All the complexity lives in the modules under &lt;code&gt;afw_core/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline in Action
&lt;/h2&gt;

&lt;p&gt;Here is what the pipeline produces when you run it against a real Jira project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jira board after execution&lt;/strong&gt;: the Epic and all linked Stories created automatically.&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%2Fql58v8bumbsiin5wxejc.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%2Fql58v8bumbsiin5wxejc.png" alt="Jira Epic and Stories" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Console output&lt;/strong&gt;: the full agent execution log showing each tool call and result.&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%2Fap6ok8ixq6x9wn6nhqcm.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%2Fap6ok8ixq6x9wn6nhqcm.png" alt="Console Output" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution report&lt;/strong&gt;: the summary file generated by the executor agent in the &lt;code&gt;output/&lt;/code&gt; directory.&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%2Fytk7q5vmm0wjmdyt4r47.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%2Fytk7q5vmm0wjmdyt4r47.png" alt="Execution Report" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Working through these four steps, several patterns emerged that are worth sharing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tools don't handle parallelism well.&lt;/strong&gt; When the LLM sends multiple tool calls in a single response, the MCP server may fail. The workaround is simple: add "ONE AT A TIME" to the agent's instructions. The agent respects this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The framework's error handling has a hidden default.&lt;/strong&gt; The &lt;code&gt;max_consecutive_errors_per_request&lt;/code&gt; parameter defaults to 3. If an agent hits 3 consecutive tool errors, it stops retrying. This is defined in &lt;code&gt;agent_framework._tools&lt;/code&gt; and caught me off guard initially. Knowing this default helps you debug "why did it stop?" scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;__init__.py&lt;/code&gt; needed.&lt;/strong&gt; Python's implicit namespace packages work fine. The key is choosing a unique directory name (&lt;code&gt;afw_core&lt;/code&gt;) that doesn't collide with installed packages. I initially tried naming directories &lt;code&gt;agents/&lt;/code&gt;, &lt;code&gt;tools/&lt;/code&gt;, &lt;code&gt;mcp/&lt;/code&gt;, but these collided with the framework's own modules. Renaming to &lt;code&gt;afw_core/agents/&lt;/code&gt; solved everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A well-defined directory structure makes a real difference.&lt;/strong&gt; Applying a clear project layout (&lt;code&gt;afw_core/&lt;/code&gt; with separate modules for agents, tools, MCP proxies, LLM clients, and models) greatly simplifies working with the framework. It keeps things organized and makes the codebase easy to extend as you add more agents and integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest gap today: no native tools.&lt;/strong&gt; This is, in my opinion, the framework's main weakness right now. Other frameworks like LangChain/LangGraph and CrewAI ship with a rich ecosystem of built-in tools (web search, PDF readers, database connectors, vector stores, and many more). With the Microsoft Agent Framework, you either build every tool yourself with &lt;code&gt;@tool&lt;/code&gt; or rely on MCP servers. For simple use cases that's fine, but for projects that need quick access to common integrations, the lack of native tools is a significant disadvantage that other frameworks still handle much better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pydantic validation between agents is cheap insurance.&lt;/strong&gt; It adds minimal overhead and catches data corruption early. Especially useful when the first agent's output is the second agent's input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent instructions are powerful.&lt;/strong&gt; You have a single &lt;code&gt;instructions&lt;/code&gt; string that gives you full freedom to express exactly what you need, including operational constraints like "never call tools in parallel."&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Takeaways
&lt;/h2&gt;

&lt;p&gt;The Microsoft Agent Framework is a solid entry point into the world of AI agent development. Its explicit, code-first approach means there are very few surprises: what you write is what gets executed. The MCP integration is first-class and makes it trivial to connect agents to external services like Jira, Confluence, or GitHub.&lt;/p&gt;

&lt;p&gt;The incremental approach I followed, from a single agent with no tools to a modular multi-agent pipeline, worked well as a learning strategy. Each step introduced exactly one new concept, making it easy to debug when things went wrong.&lt;/p&gt;

&lt;p&gt;If you are starting with AI agents and want a framework with minimal abstraction, the Microsoft Agent Framework is worth a try. The codebase in this article serves as a progressive tutorial you can follow step by step.&lt;/p&gt;

&lt;p&gt;All the code is available on &lt;a href="https://github.com/rosidotidev/MSFTAgentSample" rel="noopener noreferrer"&gt;GitHub (MSFTAgentSample)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agentframework</category>
      <category>openai</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>In-Depth Comparison: Workflow Control with LangGraph and CrewAI</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Fri, 01 Aug 2025 15:47:07 +0000</pubDate>
      <link>https://dev.to/rosidotidev/in-depth-comparison-workflow-control-with-langgraph-and-crewai-ae3</link>
      <guid>https://dev.to/rosidotidev/in-depth-comparison-workflow-control-with-langgraph-and-crewai-ae3</guid>
      <description>&lt;p&gt;(Originally published on my blog: &lt;a href="https://roby73.hashnode.dev/in-depth-comparison-workflow-control-with-langgraph-and-crewai" rel="noopener noreferrer"&gt;https://roby73.hashnode.dev/in-depth-comparison-workflow-control-with-langgraph-and-crewai&lt;/a&gt;)&lt;br&gt;
Choosing between &lt;strong&gt;CrewAI&lt;/strong&gt; and &lt;strong&gt;LangGraph&lt;/strong&gt; for multi-agent orchestration isn’t trivial, especially for developers balancing rapid prototyping with future scalability.&lt;/p&gt;

&lt;p&gt;Personally, I gravitate toward &lt;strong&gt;CrewAI&lt;/strong&gt; for its straightforward setup and developer-friendly abstractions. It lets you build production-ready agent flows in minutes, staying focused on business logic rather than complex orchestration architecture. That ease of use and quick iteration cadence make CrewAI a great fit for many projects.&lt;/p&gt;

&lt;p&gt;That said, &lt;strong&gt;LangGraph&lt;/strong&gt; has quickly become the go-to framework for large, stateful projects in production, thanks to its expressive graph-based DSL and wide adoption among major tech players. It offers granular control over state, dependencies, and memory, which is often necessary for complex or long-running workflows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Absolute Control Over Agent Steps: LangGraph vs. CrewAI
&lt;/h3&gt;

&lt;p&gt;One of LangGraph’s undeniable strengths is the &lt;strong&gt;absolute control it offers over every single step executed by agents&lt;/strong&gt; in the workflow. Thanks to its explicit graph-based architecture, you precisely define nodes, edges, and state transitions. This granular orchestration lets you monitor, intervene, and debug the entire flow of execution with full visibility.&lt;/p&gt;

&lt;p&gt;In contrast, &lt;strong&gt;CrewAI’s abstraction via agents and tasks simplifies setup and speeds development&lt;/strong&gt;, but sometimes this comes at the cost of &lt;strong&gt;less direct visibility and control over internal step execution&lt;/strong&gt;. The orchestration logic is more implicit and "black-boxed" inside the Crew abstractions, which can make fine-grained intervention more challenging.&lt;/p&gt;

&lt;p&gt;That said, with the introduction of &lt;strong&gt;Flows&lt;/strong&gt; in CrewAI—explicitly defined workflows coordinating agents and tasks—you can reclaim a similar level of control and transparency over execution. The Flows feature lets you model sequences, conditions, and branching explicitly, reducing the "black box" effect while still keeping CrewAI’s ease of use.&lt;/p&gt;

&lt;p&gt;The Java Verifier examples below provide a clear demonstration of this: while LangGraph exposes every node and edge explicitly, CrewAI Flows allow similarly controlled stepwise execution and monitoring inside a more developer-friendly environment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example: The Java Verifier
&lt;/h3&gt;

&lt;p&gt;To illustrate the difference in abstraction and workflow style between CrewAI and LangGraph, here is a minimal “Verifier” agent implementation in both frameworks. The goal of this verifier is to validate, correct, and optimize small Java code snippets. The workflow is designed with a loop: it first checks the code for syntax errors. If an error is found, it attempts to correct the code and then re-enters the verification step. This "verify-correct" loop continues until the code is deemed syntactically correct. Only then does the workflow proceed to the final step of optimizing the code for readability and performance. You can find all the code shown in this post on this &lt;a href="https://github.com/rosidotidev/Java-Verifier-LangGraph-CrewAI" rel="noopener noreferrer"&gt;Github repo (Java-Verifier-LangGraph-CrewAI)&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  1. CrewAI: Java Verifier with Flow
&lt;/h4&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%2Fnivzj4tyo6ig5fwrcw0q.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%2Fnivzj4tyo6ig5fwrcw0q.png" alt="This image has been generated using the functionality at the end of the provided Python code)" width="799" height="388"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(This image has been generated using the functionality at the end of the provided Python code)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CrewAI Flows allow you to define a multi-agent workflow in a class-based structure that feels familiar to developers. You declare agents and then use decorators (&lt;code&gt;@start&lt;/code&gt;, &lt;code&gt;@router&lt;/code&gt;, &lt;code&gt;@listen&lt;/code&gt;) to map out the flow. This structure keeps the logic contained and easy to read. For this particular example, we chose to orchestrate the individual agents directly within the &lt;code&gt;Flow&lt;/code&gt; rather than using a complete &lt;code&gt;Crew&lt;/code&gt;. This approach gives us more &lt;strong&gt;direct control&lt;/strong&gt; over each agent's step, which was ideal for this straightforward, cyclical process. It's important to note that in more complex scenarios, CrewAI's flexibility allows you to orchestrate entire &lt;code&gt;Crews&lt;/code&gt; of agents, leveraging their collective intelligence for a different level of abstraction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crewai.agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crewai.flow.flow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeValidationState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;syntax_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JavaCodeValidationFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CodeValidationState&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;

    &lt;span class="n"&gt;analyzer_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code syntax analyzer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Receive Java code and reply exactly with &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; based on syntax.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backstory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a meticulous and precise code quality tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;corrector_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code corrector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Receive syntactically incorrect Java code and return corrected code with valid syntax only. Return only Java code with no explanation and no markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backstory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an expert Java programmer who fixes code errors.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;optimizer_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code optimizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Receive syntactically correct Java code and optimize it for readability and performance without changing logic. Return only Java compilable code with no explanation and no markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backstory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior developer specializing in code optimization.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;try_again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Starts the workflow, sends the code to the analyzer.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting code analysis...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analyzer_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;syntax_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;syntax_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syntax_status&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyzer result: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;syntax_status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_analysis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;conditional_next_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Decides whether to proceed with correction or optimization.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;syntax_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;syntax_status&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conditional_next_step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;syntax_status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;syntax_status&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nd"&gt;@router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;correct_code_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Performs code correction.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corrector_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Corrector: code corrected.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;try_again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nd"&gt;@listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;optimize_code_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Performs code optimization.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimizer_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optimizer: optimization finished.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Workflow complete. Final optimized code:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example usage of the workflow
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_code_validation_flow&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Initial Java code with a syntax error (missing semicolon)
&lt;/span&gt;    &lt;span class="n"&gt;initial_java_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    public class Test {
        public static void main(String[] args) {
            System.out.println(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
        }
    }
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaCodeValidationFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting CrewAI Java code validation flow...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Start the workflow and get the final result
&lt;/span&gt;    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;initial_java_code&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;run_code_validation_flow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my_flow_plot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; In CrewAI, you first define the &lt;strong&gt;roles&lt;/strong&gt; and &lt;strong&gt;goals&lt;/strong&gt; of your agents. Then, you use a &lt;code&gt;Flow&lt;/code&gt; to orchestrate them through a series of steps. The flow itself feels like a regular class with methods, where decorators handle the routing logic. This keeps the logic contained and aligned with a developer's class-based thinking. Notice that here we are using default LLM used by CrewAI that at the minute is “gpt-4o-mini“.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. LangGraph: Java Verifier with Graph
&lt;/h4&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%2Fgvqw3ihz191v9t8o5ntu.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%2Fgvqw3ihz191v9t8o5ntu.png" alt="This image has been generated using the functionality at the end of the provided Python code" width="411" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This image has been generated using the functionality at the end of the provided Python code)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;LangGraph's approach is more explicit and foundational. You define a graph with nodes and edges, where each node is a function. The state of the workflow is passed explicitly between nodes, and conditional edges are used to manage the flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;  &lt;span class="c1"&gt;# Import TypedDict and Annotated
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;IPython.display&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display&lt;/span&gt;
&lt;span class="c1"&gt;# Ensure your OpenAI API Key is set in environment variable OPENAI_API_KEY
&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Define your state schema
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;is_correct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JavaVerifierGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Initialize LangChain OpenAI wrapper (reads OPENAI_API_KEY from env)
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_analysis_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_analysis_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optimize_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimize_code_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;correct_code_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Define entry and transitions
&lt;/span&gt;        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_analysis_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Conditional edges from analyzer node
&lt;/span&gt;        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_analysis_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conditional_next_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optimize_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optimize_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="c1"&gt;# Cycle back from corrector to analyzer
&lt;/span&gt;        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_analysis_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;



    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_check_code_syntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Check if the following Java code is syntactically correct. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reply ONLY with &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Answer:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_correct_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Correct the following Java code to be syntactically valid. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provide only the corrected code without explanations.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Corrected Java code:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_optimize_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optimize the following syntactically correct Java code &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;for readability and performance. Return only Java code with no explanation and no markdown.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Java code:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Optimized Java code:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# The analyzer node sets 'is_correct' in state to guide conditional branching,
&lt;/span&gt;    &lt;span class="c1"&gt;# no explicit goto inside the node function.
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_analysis_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyzer: Checking syntax via LLM...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm_check_code_syntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyzer: Code is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_correct&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;incorrect&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;is_correct&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Optimizer node - performs optimization and ends the workflow.
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;optimize_code_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optimizer: Optimizing code via LLM...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;optimized_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm_optimize_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optimizer: Optimization complete. Ending workflow.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;optimized_code&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Corrector node - fixes syntax errors; no goto, flow cycles via explicit edge.
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;correct_code_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Corrector: Correcting code via LLM...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;corrected_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm_correct_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Corrector: Correction complete. Returning to analyzer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;corrected_code&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Branching function to decide next node after analyzer based on 'is_correct'.
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;conditional_next_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CodeState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optimize_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_code_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;initial_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# 'analyzer' will set this value at the beginning
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting the execution of the workflow with invoke()...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Run the workflow and get the final state
&lt;/span&gt;        &lt;span class="n"&gt;final_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Workflow completed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Final Java code (corrected and optimized):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Final &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; state: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_code_validation_flow&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="n"&gt;initial_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    public class Test {
        public static void main(String[] args) {
            System.out.println(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
        }
    }
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;initial_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;initial_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# 'analyzer' will set this value at the beginning
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting the execution of the workflow with invoke()...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;JavaVerifierGraph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;final_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;run_code_validation_flow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# This command generates the PNG data as bytes, which works outside of Jupyter
&lt;/span&gt;    &lt;span class="n"&gt;mermaid_png_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_graph&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;draw_mermaid_png&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Specify the filename for the output image
&lt;/span&gt;    &lt;span class="n"&gt;output_filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langgraph_workflow.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Open the file in binary write mode ('wb')
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Write the image bytes to the file
&lt;/span&gt;            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mermaid_png_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Diagram sevaed on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error during diagram generation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; With LangGraph, you build the workflow by explicitly defining a &lt;strong&gt;state schema&lt;/strong&gt;, followed by individual &lt;strong&gt;nodes&lt;/strong&gt; (functions). You then connect these nodes with &lt;strong&gt;edges&lt;/strong&gt;. The orchestration logic is literally a graph you construct, giving you a powerful, albeit more complex, low-level tool for state management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;I appreciate CrewAI’s lean, quick-to-launch workflows and developer experience, which speed up prototyping and smaller scope projects. The &lt;code&gt;Flow&lt;/code&gt; feature adds a layer of explicit control that bridges the gap with more low-level frameworks. Meanwhile, LangGraph stands out for heavyweight, scalable AI pipelines in enterprise-grade systems with a need for full-control graph orchestration.&lt;/p&gt;

&lt;p&gt;To gain a more complete understanding of each framework's workflow implementation capabilities, other critical aspects must be considered. These include &lt;strong&gt;state management&lt;/strong&gt;, the ability to handle &lt;strong&gt;state persistence&lt;/strong&gt; (saving and resuming a workflow), and the ease of implementing a &lt;strong&gt;Human-in-the-Loop (HITL)&lt;/strong&gt; scenario. These factors are crucial for designing robust, long-running processes that require durability or external intervention.&lt;/p&gt;

&lt;p&gt;For the time being, CrewAI fits my development style best, but I watch LangGraph’s evolution with interest as it appears to be solidifying its place as the go-to framework for complex AI agent orchestration at scale.&lt;/p&gt;

</description>
      <category>langgraph</category>
      <category>crewai</category>
      <category>ai</category>
      <category>openai</category>
    </item>
    <item>
      <title>Crewai</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Wed, 30 Jul 2025 18:47:56 +0000</pubDate>
      <link>https://dev.to/rosidotidev/crewai-2nkk</link>
      <guid>https://dev.to/rosidotidev/crewai-2nkk</guid>
      <description></description>
    </item>
    <item>
      <title>Spring.ai</title>
      <dc:creator>rosidotidev</dc:creator>
      <pubDate>Sun, 27 Jul 2025 13:19:05 +0000</pubDate>
      <link>https://dev.to/rosidotidev/springai-17al</link>
      <guid>https://dev.to/rosidotidev/springai-17al</guid>
      <description></description>
      <category>spring</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
