Mo Sharif
← ~/blog

How AI Turns a Text Prompt Into an Architecture Diagram

"Build me an e-commerce platform with real-time inventory and payment processing."

That sentence becomes a 15-20 node interactive architecture diagram on a React Flow canvas in about four seconds. This post is for the engineers who want to know exactly how that pipeline works. No hand-waving. No "AI magic." The actual stages, the actual problems, and the code-level decisions that make it work.

A prompt-to-diagram pipeline

is a system that converts a natural-language description of software into a schema-validated graph of nodes and edges, positions that graph automatically, and renders it as an editable diagram.

If you want to try it first: codelit.io. Then come back and see what's happening under the hood.

Codelit runs every prompt through five stages in a fixed order: intent parsing, pattern selection, structured generation, layout computation, and canvas rendering. Each stage hands a stricter data structure to the next one, and each stage has its own failure modes.

StageWhat goes inWhat comes outWhere the difficulty sits
1. Intent parsingRaw prompt textComponent list plus modifiersThe prompt is vague and the defaults have to be defensible
2. Pattern selectionComponent listA topology (microservices, monolith, event-driven)Catching cues like "keep it simple" that change the whole shape
3. Structured generationComponents plus patternNode and edge JSONMaking an LLM emit valid, schema-conformant output every time
4. Layout computationNode and edge JSONCoordinates for every nodeLooking like an architecture diagram, not a plate of spaghetti
5. Canvas renderingPositioned graphInteractive React Flow canvasKeeping every node and edge type distinct and still editable

Let me walk through them.

Codelit's intent parser maps a domain phrase like "e-commerce platform" onto a known component set — product catalog, shopping cart, checkout, payment processing, order management, user auth — and then applies modifiers found in the rest of the prompt.

"Build me an e-commerce platform" is vague. Does it include a mobile app? Admin dashboard? Analytics? Recommendation engine?

I don't try to read minds. The parser extracts explicit requirements and infers reasonable defaults. Modifiers change the output from there. "With real-time inventory" adds a WebSocket layer and an inventory service with event-driven updates. "And payment processing" adds a payment gateway (Stripe-like) with webhook handling.

I built this as a two-pass system:

Pass 1: Keyword extraction and domain mapping. Fast, deterministic, zero AI cost. Handles the 80% case.

Typescript
// Simplified domain mapping
const domainMap: Record<string, string[]> = {
  "e-commerce": ["product-catalog", "cart", "checkout", "payment", "orders", "auth"],
  chat: ["messaging", "websocket", "presence", "auth", "storage"],
  social: ["feed", "posts", "comments", "notifications", "auth", "cdn"],
};

// Modifier detection
const modifiers: Record<string, string[]> = {
  "real-time": ["websocket-server", "event-bus"],
  payment: ["payment-gateway", "webhook-handler"],
  ml: ["ml-service", "feature-store", "model-registry"],
};

Pass 2: Send ambiguous elements to the LLM for classification. Only fires when Pass 1 can't resolve the intent confidently. This keeps the common cases fast and cheap.

Because the selected pattern requires them. When Codelit picks microservices for a multi-domain e-commerce app, it also inserts an API gateway, a message broker, and load balancers. Nobody typed those into the prompt, but the architecture doesn't work without them.

Once I know the components, I need to decide how they connect. This is where architectural knowledge gets encoded. I maintain a library of patterns — monolith, microservices, event-driven, CQRS, serverless, hybrid — and each pattern defines a topology: how services communicate, where data flows, what infrastructure sits between them.

E-commerce with multiple domains (catalog, cart, payment, inventory) naturally fits microservices with an API gateway. But if the user says "keep it simple" or "this is for a hackathon," the system selects a modular monolith instead. That single phrase changes every downstream stage.

You stop asking nicely and start validating. Codelit sends the target schema with the prompt, uses native structured-output modes where a provider offers them, parses the response with Zod, and feeds validation errors back to the model for up to three retries.

This is where most of the engineering effort lives. The LLM needs to produce a JSON object defining every node (type, label, properties, tier) and every edge (source, target, label, type). Getting LLMs to produce consistent, valid, schema-conformant JSON is genuinely hard. I tried three approaches before finding one that works.

ApproachHow well it workedWhere it brokeVerdict
Free-form prose, parsed afterwardsBrittle from day oneThe model used different names for the same service across paragraphs, and the parser couldn't handle the variationScrapped after two weeks
JSON schema described in the prompt~80% of responsesThe other 20%: commentary outside the JSON block, incorrect nesting, hallucinated fieldsNot good enough
Schema enforcement with a Zod validation loopOver 99%Only on prompts that contradict themselvesWhat I ship today

The prompt includes the target schema. I use structured output modes where available (OpenAI's response_format, Anthropic's tool use). When the response comes back, I validate it with Zod:

Typescript
import { z } from "zod";

const NodeSchema = z.object({
  id: z.string(),
  type: z.enum(["database", "service", "queue", "cache", "gateway", "client", "external"]),
  label: z.string(),
  tier: z.enum(["client", "edge", "application", "data", "external"]),
  properties: z.record(z.unknown()),
});

const EdgeSchema = z.object({
  id: z.string(),
  source: z.string(),
  target: z.string(),
  label: z.string().optional(),
  type: z.enum(["sync", "async", "data-flow", "event"]),
});

const ArchitectureSchema = z.object({
  nodes: z.array(NodeSchema),
  edges: z.array(EdgeSchema),
});

If validation fails, I send the Zod error back to the LLM with the original prompt and ask it to fix the output. That loop runs up to three times. The remaining failures are prompts that are genuinely contradictory ("build me a serverless monolith," and sure, I can try).

Stage 3 returns a flat JSON object with a nodes array and an edges array. Every node carries a type and a tier. Every edge carries a source, a target, and a type. For the e-commerce prompt, simplified, it looks like this:

Json
{
  "nodes": [
    { "id": "client", "type": "client", "label": "Web App", "tier": "client" },
    { "id": "gateway", "type": "gateway", "label": "API Gateway", "tier": "edge" },
    { "id": "catalog", "type": "service", "label": "Product Catalog", "tier": "application" },
    { "id": "cart", "type": "service", "label": "Shopping Cart", "tier": "application" },
    { "id": "payment", "type": "service", "label": "Payment Service", "tier": "application" },
    { "id": "inventory", "type": "service", "label": "Inventory Service", "tier": "application" },
    { "id": "orders-db", "type": "database", "label": "Orders DB (PostgreSQL)", "tier": "data" },
    { "id": "cache", "type": "cache", "label": "Redis Cache", "tier": "data" },
    { "id": "queue", "type": "queue", "label": "Event Bus (RabbitMQ)", "tier": "data" },
    { "id": "stripe", "type": "external", "label": "Stripe", "tier": "external" }
  ],
  "edges": [
    { "id": "e1", "source": "client", "target": "gateway", "type": "sync" },
    { "id": "e2", "source": "gateway", "target": "catalog", "type": "sync" },
    { "id": "e3", "source": "gateway", "target": "cart", "type": "sync" },
    { "id": "e4", "source": "cart", "target": "payment", "type": "sync" },
    { "id": "e5", "source": "payment", "target": "stripe", "type": "sync", "label": "Charge" },
    {
      "id": "e6",
      "source": "inventory",
      "target": "queue",
      "type": "event",
      "label": "Stock Updated"
    },
    { "id": "e7", "source": "catalog", "target": "cache", "type": "data-flow" },
    { "id": "e8", "source": "payment", "target": "orders-db", "type": "data-flow" }
  ]
}

That typed graph drives everything downstream: layout, rendering, styling, interactivity. It's also the graph chaos mode walks when it propagates a cascading failure, which only works because every edge already knows whether it carries a synchronous call or an event.

Codelit lays the graph out with dagre, a JavaScript implementation of the Sugiyama algorithm for layered graphs. Nodes are grouped by tier and assigned to horizontal layers, then ordered within each layer to minimise edge crossings.

On top of dagre, I apply architectural conventions as post-processing heuristics:

  • Databases always go at the bottom
  • Load balancers center above the services they front
  • Message queues sit between producers and consumers
  • External services go on the right edge

This is the same problem I hit converting Mermaid syntax into an interactive canvas: the source tells you what connects to what and never where anything sits. Whatever invents the geometry has to be deterministic — same input, same output. If you regenerate the same architecture and the boxes land somewhere else, you stop believing the diagram means anything.

React Flow renders the positioned graph, and the node and edge types from Stage 3 map straight onto visual treatments. The schema value decides the shape, the stroke and the animation, so nothing on the canvas is decorative.

ElementSchema typeHow it renders on the canvas
NodedatabaseCylinder
NodeserviceRounded rectangle
NodequeueDistinctive queue icon
NodeexternalCloud boundary
EdgesyncSolid arrow
EdgeasyncDashed line
Edgedata-flowThicker line
EdgeeventAnimated dashes

Every element is interactive: click, drag, right-click to modify. The custom node components, the animated edges and the layout wiring are a subject of their own, and I pulled them apart in building the interactive architecture canvas with React Flow. The diagram is a starting point, not a finished product.

When a user follows up with "add caching," Codelit modifies the existing diagram instead of generating a new one. The model receives a compressed summary of the current canvas plus the modification request, and returns a diff: nodes to add, edges to add, properties to change.

The naive approach is to generate a completely new diagram. That throws away every manual adjustment — repositioned nodes, renamed services, added annotations — and users notice immediately. Correct behaviour is narrow: add a Redis node, connect it to the relevant services, leave everything else untouched.

Context management is critical here. A 30+ node architecture with full properties eats a massive chunk of the context window. I aggressively summarise: the LLM doesn't need every property, just the structural skeleton. Detailed properties stay client-side and get merged back after the diff.

Conversation history handles reference resolution across turns. "Make that database a cluster," which database? The one from the last turn. "Add a cache in front of it," in front of what? The service we just discussed. That context flows into every prompt.

Because one provider is one point of failure, and because each model has different structured-generation habits. Codelit routes across 11 providers with automatic fallback, so a slow or unavailable model reroutes without the user seeing anything except a diagram.

I maintain provider-specific prompt variants. One model tends to over-generate and adds components nobody asked for. Another under-specifies edges, creating nodes but forgetting to connect them. The prompt variants compensate for those tendencies. The routing itself, and why a single provider is a liability, is a longer story I told in how Codelit's multi-provider fallback works.

The Zod validation loop normalises everything. Whichever model generated the output, it passes the same validation, and everything downstream just receives valid data. Provider differences don't leak past Stage 3.

Two things. I'd write the JSON schema and the Zod validation on day one, and I'd build the diff-based follow-up system before the first release. Both were retrofits, and both cost more to add late than they would have cost to start with.

I wasted months wrestling with inconsistent LLM output before realising that enforcing a structured contract is an engineering problem, not an AI problem. Retrofitting statefulness onto a pipeline that assumed every generation was independent was the same lesson in a different costume.

The pipeline keeps evolving. I'm working on multi-step generation for complex architectures (data layer first, then application, then edge) instead of generating everything in one shot. Early results show better separation of concerns.

The same structured-output backbone drives two more canvases now: product boards and agent workflows. You describe an agent job once, and Codelit designs the production version, model routing and approval gates included. If you want the rest of that story, how Codelit got built starts with a Lucidchart rage-quit.

Try it yourself at codelit.io and check out the full feature set. The gap between natural language and structured diagrams is absolutely bridgeable. You just need a pipeline that's engineered, not vibed.

Questions people actually ask

How does AI turn a text prompt into an architecture diagram?
In five stages. An intent parser maps the prompt onto a known set of components, a pattern selector picks a topology such as microservices or a modular monolith, a language model generates node and edge JSON against a fixed schema, a layout engine assigns coordinates to every node, and a canvas library renders the result as an editable diagram. Each stage hands a stricter data structure to the next.
How do you stop an LLM from returning invalid JSON?
You validate instead of prompting harder. Codelit sends the target schema with the request, uses a provider's native structured output mode where one exists, then parses the response with Zod. If validation fails, the validation error goes back to the model alongside the original prompt and it tries again, up to three times. Prompt-only structured output worked about 80 percent of the time. Schema enforcement plus the retry loop pushed that over 99 percent.
How long does it take to generate an architecture diagram from a prompt?
About four seconds, producing an interactive diagram of roughly 15 to 20 nodes. Most of that time is the language model generating structured JSON. The first parsing pass is keyword based and costs nothing, and layout is a deterministic computation over the returned graph rather than another model call. A failed validation adds a round trip, because the error is sent back to the model for correction.
Can you edit an AI generated architecture diagram without regenerating it?
Yes, and that matters more than it sounds. Regenerating from scratch throws away repositioned nodes, renamed services and annotations the user added by hand. Codelit sends a compressed summary of the current canvas along with the follow up request, and the model returns a diff rather than a new diagram: nodes to add, edges to add, properties to change. Detailed properties stay client side and get merged back in.
Why route diagram generation across multiple AI providers?
A single provider is a single point of failure, and models differ in how they handle structured generation. One over generates and adds components nobody asked for. Another creates nodes but forgets to connect them. Codelit keeps provider specific prompt variants to compensate, routes across 11 providers, and falls back automatically when one is down or slow. Schema validation normalises whatever comes back, so provider differences never reach the renderer.
What layout algorithm positions the nodes in a generated diagram?
Codelit uses dagre, a JavaScript implementation of the Sugiyama method for layered graph drawing. Nodes are grouped by tier into horizontal layers and ordered within each layer to minimise edge crossings. Architectural conventions are then applied as post processing: databases sink to the bottom, load balancers centre above the services they front, queues sit between producers and consumers, and external services move to the right edge. The layout is deterministic, so the same input always produces the same positions.