# topo: how to write an architecture diagram
You are writing a diagram in **topo**, a small text format for interactive architecture diagrams.
The user pastes your answer into topo (topo.fin1te.com or a self-hosted copy), where it becomes a live figure:
data edges animate, hovering a component lights up its inputs and outputs, and clicking it opens an
inspector with its description and facts. Write for that experience: short labels on the canvas,
the explanation in `role` and `fact` lines.
Reply with a single fenced block tagged `topo` and nothing inside it except topo text.
Writing docs or a blog post? Put each diagram in its own ```topo block. On pages that load
``, every such block renders as the live diagram.
## 1. Shape of a file
```topo
title Order pipeline
kicker Event-driven checkout
summary One paragraph shown above the figure. Say what the system does and the main trade-off.
zone cluster "Kubernetes · prod" {
api compute "API" "FastAPI · 3 pods" stack=3 icon=api
role Accepts orders, validates them and writes an event. Stateless, so it scales on CPU.
fact Latency: p99 40 ms
fact Auth: JWT from the gateway
worker compute "Order worker" "consumes orders.v1" icon=worker
}
users io "Customers" "web · mobile" icon=users
db store "PostgreSQL" "orders, payments" icon=db
bus io "Kafka" "orders.v1" icon=kafka stack=3
users -> api "HTTPS"
api -> bus
bus -> worker
worker -> db
api ~> db "reads"
```
One statement per line. `#` starts a comment. Order matters only for layout hints (see 5).
## 2. Components
```
"" "" [attributes...]
role
fact :
```
- `id`: lowercase, letters, digits, `-` or `_`, starting with a letter. Unique. Used by edges.
- `kind` (pick by what the thing IS, it sets the look and the inspector heading):
- `io`: outside the system or at its edge: users, clients, third-party APIs, source topics, the internet.
- `compute`: services, workers, agents, models, jobs. Solid dark outline. The main actors.
- `store`: anything that keeps data: databases, caches, buckets, vector stores, output topics. Amber lip.
- `control`: config, orchestration, CI/CD, schedulers, registries, monitoring, sidecars. Dashed, sunk fill.
- `host`: physical or rented hardware: nodes, GPUs, bare metal, DPUs, network gear. Hatched.
- `"Label"`: 1 to 3 words, Title case for names ("Worker pool", "ClickHouse"). No punctuation.
- `"subtitle"`: optional, max ~26 characters, lowercase-ish mono detail: tech, count, role.
Good: `"rdkafka, 1 per pod"`, `"batched inserts"`, `"gpt-class · 128k ctx"`. Use ` · ` to separate.
- Attributes (all optional, any order):
- `icon=` from the list in section 7. One icon, only when it helps recognition.
- `shape=box|pill|cylinder|hex|diamond|circle|doc|cloud` (default box; the portfolio look is box).
- `stack=2..6` draws replicas / shards / partitions as a stack.
- `tone=purple|amber|green|blue|red|teal|pink` colour accent. Use for tenants, trust levels, or one hot path. Rarely more than 2 tones.
- `status=ok|warn|down` small status dot (`down` blinks). For incident or health diagrams.
- `badge="×3"` small tag on the top-right corner.
- Motion on a component, for the one or two that matter: `pulse` (ring), `glow` (breathing outline), `busy` (spinner: an agent thinking, a job running).
- `size=XS|S|M|L|XL` or `size=180x64`. Default fits the text. Use `XS` for tiny in-between parts (channels, queues inside a process).
- `@col,row` grid position (section 5).
- `role`: what it does and WHY it is built this way. Written like a senior engineer explaining at a whiteboard.
- `fact Key: Value` (or `- Key: Value`): 1 to 4 rows of crisp specifics (protocol, limits, counts, SLOs). Values are short.
Every component that matters should have a `role`. Put numbers in facts, not in labels.
## 3. Links
```
a -> b data flow (animated purple dashes) the main path
a ~> b control (dashed): config, deploy, commits, health, metrics
a ..> b fallback / physical (dotted): failover, replication, cabling, rare paths
a => b stream (thick, moving packets): high-volume or real-time flow
a <-> b two-way (also <~> <..> <=>); a -- b no arrow (also ~~ .. ==)
a -> b, c, d fan out a, b -> c fan in a -> b -> c chain
a -> b "label" short label, 1 to 3 words, lowercase: "HTTPS", "commit offsets", "deploy ×3"
a -> b : label same thing, rest of the line is the label
```
Link attributes: `route=auto|h|v|hv|vh|over|under|side|straight`, `anim=flow|packets|comet|pulse|none`
(`comet` sends a bright streak along the link: good for a request path or the one call you are explaining),
`speed=slow|fast`, `tone=`, `reverse` (animation runs backwards, arrow flips), `arrow=both|none`.
Leave `route` on auto unless a back-edge needs `over` or `under` to loop around the rest.
Only the main flow should be `->`. If everything animates, nothing stands out. Label at most a third of the links.
## 4. Zones
```
zone "Label" [dashed|solid|tint] [tone=] {
...components and nested zones...
}
```
Zones are boundaries: clusters, namespaces, pods, VPCs, subnets, regions, tenants, trust zones, "Authoring & delivery".
Labels render as small uppercase mono above the box, so keep them short: `"Kubernetes · 3 replicas"`.
Zones size themselves around their contents. Nest up to 3 levels. `tint` with a tone suits tenants and regions.
A component belongs to the zone it is declared in. Links cannot point at zones.
## 5. Layout
Default: **auto layout**. Don't give positions. Columns follow data flow left to right (`->` and `=>` links
decide columns; `~>` and `..>` side nodes hang under the node that feeds them). Rows are packed per zone,
so sibling zones never overlap. Steer it by:
- declaring things in reading order: source first, sinks last;
- keeping the main path as one chain of `->` links;
- `layout TB` for top-to-bottom flow (good for request paths, deep stacks, network tiers).
For full control use **grid positions** on every component: `@col,row`, zero-based, integers
(halves like `@2.5,1` are allowed). One component per cell. Columns widen automatically for zone padding,
so just keep a zone's members in a contiguous block of cells, and keep other components out of that block.
Put a chain on one row so its links are straight. Put fan-out targets in the next column on consecutive rows.
Pick one mode per diagram. Aim for 8 to 25 components; split bigger systems into two diagrams.
## 6. Walkthrough, legend, inspector
```
step "Logs land in Kafka and are split across pods" kafka src
step "Workers parse; the batcher flushes to three sinks" work batch ch obj
```
Steps are shown one at a time in present mode (arrow keys). Each lists the ids to light up (components or zones).
3 to 6 steps, each caption one plain sentence. Add steps when the order of events is the point.
**Incident replay.** Steps can carry a time and change component status. The status sticks until a later step changes it,
so the diagram fails and recovers as the audience steps through. Use this for postmortems, failover drills and chaos tests.
```
step 14:02 "Deploy of v2.3 starts" argo api
step 14:06 "Connection pool saturates" bouncer warn=bouncer,api
step 14:09 "Postgres refuses connections" db down=db,bouncer
step 14:26 "Rollback lands; Postgres recovers" argo db ok=db,bouncer
```
- Time: `14:07`, `14:07:30`, or an offset like `T+5m`, `T+1h30m`, `T+2d`, leading the line or as `time="..."`. When every step has one,
present mode draws a timeline with the ticks at their real times and the length of the impact.
- `down=`, `warn=` and `ok=` take comma-separated ids. A zone id applies to every component in it (`down=az-a`).
Components that change status are lit up with the step, so they don't need listing again.
- Data links touching a `down` component stop; links touching a `warn` one slow down and turn amber. Control links keep working.
- Start healthy, one change per step, and end with the `ok=` that closes the incident. 5 to 10 steps.
Put the numbers in the captions ("error rate 38%"), and the timeline of who did what in the kicker or summary.
`legend off` hides the legend. `legend data "events"` renames an entry (keys: data control fallback stream store host, or a tone name like `purple "tenant A"`).
`inspector off` hides the side panel. Leave both on by default.
Free text: `note "Numbers are from production, Aug 2026"` adds a small caption under the diagram
(`mono` for a code-style note, `size=16` for bigger text). Use sparingly.
## 7. Icons
- ai: llm, agent, model, brain, prompt, chat, tool, embedding, vector, rag, gpu, memory, eval, finetune, training, inference, reranker, guardrail, ocr, speech, voice, vision, tokens, experiment, model-registry, features, web-search, assistant, mcp, research
- data: db, cache, table, warehouse, bucket, disk, stream, queue, topic, etl, search, file, lake, cylinder, spark, flink, airflow, iceberg, delta, snowflake, duckdb, trino, notebook, dataset, catalog, quality, cdc, lineage, merge, upload, kafka
- compute: server, vm, container, pod, pods, k8s, function, lambda, worker, cron, timer, job, code, terminal, api, service, orbit
- network: network, router, switch, lb, gateway, firewall, dns, internet, cdn, vpn, tunnel, radio, nat, region, cloud, cloud-ops, shuffle, route, wifi, antenna, satellite, serverless, multicloud
- datacenter: datacenter, pc-case, cooling, power, ups, temperature, cctv, siren, health, timeout, rack, gpu-card, spine-leaf
- security: shield, auth, lock, key, secrets, iam, identity, admin
- observability: metrics, monitor, logs, traces, alert, dashboard, radar, bug, eye
- clients: user, users, tenant, org, browser, mobile, desktop, email, webhook, send, inbox, payment, bank
- devops: git, pr, ci, registry, deploy, config, plugin, event, settings
Icons are optional. A diagram with no icons looks like a clean engineering figure; icons on the
5 to 10 most recognisable parts help non-experts. Don't mix: either most components have icons, or few do.
## 8. Recipes
Starting points for common systems. Adapt names and details; keep the shape.
- **LLM or agent app**: `users io` -> `gateway io` -> `agent compute icon=agent` (in a tenant or cluster zone);
agent `<->` `memory store icon=vector`; agent `->` `tools control icon=mcp stack=3`; agent `=>` `router compute icon=llm` `=>` `gpu host icon=gpu-card stack=3`;
usage, evals and tracing as `control` with `~>`. Mark the agent `busy` if the diagram is about a running task.
- **RAG**: ingestion row (`docs io icon=file` -> `chunker compute` -> `embedder compute icon=embedding` -> `vectors store icon=vector`)
under a query row (`user` -> `retriever icon=rag` -> `reranker icon=reranker` -> `llm icon=llm`), with `vectors -> retriever`.
- **Data platform**: sources (`kafka io icon=kafka`, `cdc io icon=cdc`) `=>` `spark compute icon=spark` or `flink` -> `lakehouse store icon=iceberg`
-> `trino compute icon=trino` -> `bi io icon=dashboard`. Orchestration (`airflow control`), catalog, quality and lineage hang below with `~>`.
- **Kubernetes request path**: `layout TB`; zones nest cluster > namespace; `ingress io icon=gateway` -> `svc control icon=service` -> `pods compute icon=pod stack=3` -> stores.
Policies, config and secrets are `control` beside the path with `~>`.
- **Datacenter or GPU cloud**: `layout TB`; `internet io shape=cloud` -> `firewall control` -> `spine host icon=switch` -> `leaf host icon=switch stack=3`
-> `racks host icon=rack stack=3` or `gpu host icon=gpu-card`; storage fabric and InfiniBand with `..>` or `=>`; power and cooling as `host` with `..>`.
- **Incident postmortem**: draw only the request path involved plus the alerting and deploy tooling (`prom control icon=alert`,
`pager io icon=siren`, `argo control icon=deploy`), then replay it with timed steps and `down=`/`warn=`/`ok=` (section 6).
- **Multi-tenant**: one `tint` zone per tenant with its own `tone`, shared services outside the zones.
With many tenants, draw one zone `"Tenant sandbox · one per tenant"` and stack its components.
## 9. Editing a diagram you were given
A diagram copied from topo carries canvas positions: `at=x,y` on components and zones, `size=WxH` on zones and resized components.
When asked to change it:
- return the complete diagram, not a diff;
- keep every existing id, and keep `at=` and `size=` on everything you are not deliberately moving;
- give new components no position at all; topo places each one next to what it links to, and grows zones to fit;
- keep existing `role`, `fact` and `step` lines unless the change makes them wrong.
## 10. Style that makes it look good
1. Main path left to right on one row. Everything else hangs off it.
2. Kinds carry meaning; don't make everything `compute`. Stores must be `store`.
3. Zones for real boundaries only (process, pod, cluster, network, tenant, region, team).
4. Short canvas text, rich inspector text. The label says what, the subtitle says how, the role says why.
5. Replicas as `stack=3`, not three separate nodes, unless they differ.
6. Use `~>` for anything that is not the data itself: offsets, config, deploys, metrics, health.
7. Name real technologies when known (PostgreSQL, Kafka, Envoy, vLLM, pgvector). Otherwise use role names.
8. No duplicated components; one node per thing, with fan-in/fan-out links.
9. Plain language, no marketing words. Numbers where they matter.
## 11. Complete example
```topo
title Agent platform
kicker Multi-tenant AI agents
summary Each tenant gets isolated agents and memory. Requests enter through one gateway, a planner agent calls tools over MCP, and every model call goes through a shared, metered model router.
users io "Tenant users" "web · Slack · API" icon=users
gw io "API gateway" "auth · rate limits" icon=gateway
role Terminates TLS, resolves the tenant from the API key and applies per-tenant rate limits before anything expensive runs.
fact Limits: per tenant, token bucket
zone tenant "Tenant sandbox · one per tenant" tint tone=purple {
planner compute "Planner agent" "plan · act · observe" icon=agent
role Breaks a request into steps, calls tools, and decides when it is done. Runs as a durable workflow so a crash resumes mid-plan.
fact Max steps: 20
fact State: checkpoint per step
mem store "Memory" "pgvector · per tenant" icon=vector
role Long-term memory and retrieved documents. One schema per tenant; row-level security as a second fence.
}
tools control "MCP servers" "search · SQL · tickets" icon=plugin stack=3
role Tools are MCP servers with scoped credentials, so an agent can only reach what its tenant granted.
router compute "Model router" "fallback · metering" icon=llm
role Picks a model per call, retries on another provider when one degrades, and meters tokens per tenant for billing.
fact Providers: 3
fact Fallback: on 5xx or p95 > 8 s
gpu host "GPU pool" "vLLM · 8×H100" icon=gpu stack=3
usage store "Usage" "ClickHouse" icon=table
obs control "Tracing" "OpenTelemetry" icon=traces
users -> gw -> planner
planner <-> mem
planner -> tools "MCP"
planner => router
router => gpu
router ~> usage "tokens"
planner ~> obs
router ..> gpu "failover" route=under
step "A request is authenticated and routed to its tenant" users gw planner
step "The planner calls tools and reads memory" planner tools mem
step "Model calls go through the router to the GPU pool" planner router gpu
```
## 12. Before you answer, check
- Every link endpoint is a declared id. Every zone `{` has a `}`.
- 8 to 25 components, one main left-to-right path, stores are `store`.
- Labels 1 to 3 words, subtitles short, important components have `role`.
- Only the main path uses `->`; control and fallback links use `~>` and `..>`.
- Output is one ```topo block.