Archer — Language Specification
Archer is a textual language for describing software architecture. An .arch file is
the single source of truth for an architecture document: the canvas renders it, edits
on the canvas compile back into it, and it is handed to coding agents, with a short
reading guide, as an implementation spec.
1. Design principles
- Agent-first. The primary readers and writers of raw Archer are AI coding agents. Humans interact through the canvas. Syntax choices favor unambiguous parsing and generation over hand-writing ergonomics, but the language stays Mermaid-adjacent and skimmable.
- The file is the truth. The canvas is a view. Every canvas edit is a file edit.
- Semantics and presentation are quarantined from each other. Everything an agent
needs to build lives in the semantic sections. Everything about pixels lives in the
layoutblock, which is machine-managed. Agent exports include it so the agent sees the diagram the author drew, but it is never architecture: agents MAY read it for context, MUST NOT implement anything from it, and SHOULD NOT write it unless asked to arrange the diagram. Two things outside it are there for readers, but belong to the document in every view, so agents write them like any other field: a component'sshape,iconandcolor(§3.3) say what a thing is at a glance (a cylinder is a store, a person is a user), and planes (§3.4) say which things belong together (a frontend and a backend, a cloud account, a network). - Strict references, permissive semantics. Every reference must resolve — a connection to an undeclared component is a validation error, never an auto-created node (deliberately the opposite of Eraser's parser). But the language imposes no compatibility rules: any component may connect to any component, types are optional, and untyped generic boxes are first-class.
- Structured where it pays, freeform where it doesn't. JSON, YAML, and table payloads are syntactically validated. Markdown payloads are freeform. Per-technology validation (e.g. linting a Postgres node's schema as real DDL) is deferred to a later version.
2. File structure
An .arch file has up to four sections, in this order:
arch "Title" v0.2 # 1. header (required)
component ... # 2. declarations: components and planes (the tree)
plane ...
a -> b ... # 3. connection declarations
layout { ... } # 4. presentation (machine-managed, optional)
- Encoding is UTF-8. Comments run from
#to end of line (outside strings and payload fences). - Section order is enforced by the formatter, not the parser: connections may appear interleaved with components in input, but the canonical form (what the app saves) always groups them as above.
2.1 Header
arch "Acme Platform" v0.2
v0.2 is the spec version the file targets. Parsers refuse files with a major
version they don't understand.
3. Components
component <id> ["Display Name"] [@<type>] [{ <body> }]
<id>— required.snake_case([a-z][a-z0-9_]*), unique within its parent scope (like keys in a JSON object). IDs are stable identity: renaming a display name never changes an id."Display Name"— optional quoted string shown on the canvas. Defaults to the id.@<type>— optional type tag (see §5). Untyped components render as generic boxes/shapes and are fully valid.- Body — optional
{ ... }block containing, in any mix:shape <name>— how the canvas draws the component:cylinder,cloud,person,robot, … (§3.3). At most one per component (a second one is errorE004). Without one, a component is a box.icon <name>— the icon the component shows:database,shopping_cart,credit_card, … (§3.3). At most one per component (a second one is errorE004). Without one, a typed component shows its type's icon and an untyped one shows none.color <name>orcolor "#rrggbb"— the color the component is drawn in:blue,green,rose, … or any hex color (§3.3). At most one per component (a second one is errorE004). Without one, a component takes its type's color.desc "..."— human-readable description (shown in the detail view). One per component. Multiline via triple quotes:desc """ ... """.src "..."— where the component lives in code: a repo-relative path to a file or a folder (apps/api/src/billing,apps/api/src/billing/invoices.ts) or a URL. Single-line string only. A component that lives in several places has onesrcline for each, kept in the order written. Agents start here when implementing or verifying the component; the app renders each one as a link. Cite only where the component itself is implemented — its entry points, its own folder — not every file it touches: shared helpers, UI primitives (a table or button component), imported libraries and utilities it merely uses are not sources for it. Prefer one folder over listing the files inside it; a fewsrclines is normal, a dozen means the component is too broad or the list too detailed.- Child
componentdeclarations — any component can hold components, arbitrarily deep, with JSON-object nesting semantics (§3.1). - Child
planedeclarations — regions on the component's own canvas (§3.4). - Payload blocks (§4).
3.1 Nesting and paths
Components form a tree. A component is referenced by its dot-path from the root:
component orders "Orders" @service {
component api "Orders API" @service
component db "Orders DB" @postgres
}
component web "Web App" @vercel.nextjs
web -> orders.api @http
orders.api -> orders.db @sql
Nesting is containment: the nested components are parts of the one that holds them. Any component can hold components, at any depth, and each one's inside is a canvas of its own:
- On the canvas where it sits, a component with nested components is one box, with a count of what's inside. Opening it (drilling down) shows its parts on a canvas of their own, with the outside components they connect to drawn along the sides.
- A connection is drawn on every canvas that shows both ends. Where a nested end isn't
visible, the arrow goes to the component that holds it: at the top level above,
web -> orders.apiis an arrow from Web App into Orders. - A connection can join a nested component (
orders.api) or the whole component (web -> orders) — say whichever is true.
Nesting is for parts that belong to a thing: a service's API and database, a cluster's services. To group things only to show where they are (a frontend and a backend, a cloud account, a network), use a plane (§3.4): it draws a region around them, keeps them all on one canvas and doesn't change their paths.
3.2 Example
component db "Primary Postgres" @postgres {
shape cylinder
desc "Single Postgres instance holding all app data. Row-level security on."
component users_table "users" @postgres.table {
schema table ```
| column | type | notes |
| ---------- | ----------- | --------------- |
| id | uuid | pk |
| email | text | unique |
| team_id | uuid | fk -> teams.id |
```
}
}
Granularity is the author's choice: one @postgres node with a schema payload, or
individual @postgres.table children, or both.
3.3 Shapes
A component is drawn as a box unless its body names a shape:
component db "Orders" @postgres {
shape cylinder
desc "Every order and its line items."
}
component support "Support Agent" @agent {
shape robot
}
component nightly "Nightly Export" @worker {
shape clock
}
The shapes, in the groups the editor's pickers show them in:
| group | shape | usually stands for |
|---|---|---|
| Basic | box |
anything (the default) |
pill |
starts, ends and states | |
circle |
events, triggers and endpoints | |
diamond |
decisions and routing | |
hexagon |
services and processing steps | |
parallelogram |
inputs, outputs and data | |
chevron |
steps and pipeline stages | |
subroutine |
functions and subprocesses | |
note |
notes and annotations | |
| People | person |
users, customers and roles |
people |
teams, groups and audiences | |
robot |
AI agents, bots and automations | |
building |
companies, partners and on-prem sites | |
| Apps & devices | browser |
web apps and sites |
phone |
mobile apps, tablets and devices | |
desktop |
desktop apps, consoles and workstations | |
laptop |
laptops and developer machines | |
terminal |
CLIs, shells and scripts | |
chip |
hardware, IoT and embedded devices | |
| Network & security | cloud |
cloud providers, external services, the internet |
globe |
the web, DNS, CDNs and regions | |
firewall |
firewalls, WAFs and network boundaries | |
shield |
security, guardrails and policies | |
lock |
auth, secrets and encryption | |
| Compute | server |
servers, hosts and VMs |
stack |
replicas, pools and fleets | |
cube |
containers, pods and build artifacts | |
module |
modules, libraries and packages | |
gear |
workers, jobs and background tasks | |
clock |
schedulers, cron jobs and timers | |
puzzle |
plugins, extensions and integrations | |
brain |
AI models, LLMs and ML | |
| Data & storage | cylinder |
databases and other stores |
bucket |
object storage and blobs | |
table |
tables, datasets and spreadsheets | |
memory |
caches and in-memory stores | |
archive |
backups, archives and cold storage | |
document |
files, reports and specs | |
folder |
file shares and directories | |
book |
docs, wikis and knowledge bases | |
search |
search engines and indexes | |
funnel |
filters, pipelines and ETL | |
| Messaging & monitoring | queue |
queues, streams and logs |
envelope |
email, inboxes and messages | |
chat |
chat, SMS and conversations | |
bell |
alerts, notifications and on-call | |
gauge |
metrics, monitoring and SLOs |
- A shape is independent of the type: the type still picks the icon and color drawn inside it, and any shape goes with any type (or none).
- The shape fills the component's box, whatever its size, and a component with nested components keeps its shape. Planes have no shape.
- A shape is a hint for readers, never a requirement: it doesn't change what a component is, and agents implementing a document may ignore it.
- A name the language doesn't know is a warning (
W304), not an error, and the component is drawn as a box, so files stay readable by tools that predate a newer shape.
Icons
A component shows its type's icon (or the product's logo), and an untyped component shows none, unless its body names an icon:
component cart "Cart" {
icon shopping_cart
}
component ledger "Ledger" @postgres {
shape cylinder
icon receipt
}
- Icons are Phosphor icons, by name in snake case
(
shopping_cart,user_circle,git_pull_request), plus the names of the catalog's own icons (database,server,queue, …). Where a catalog icon is a Phosphor icon under another name, both names work (serverandhard_drives). The MCP toollist_typeslists every icon, in the groups the editor's picker shows them in. - An icon replaces the type's icon (or logo), in the type's color; any icon goes with any type and any shape. Planes have no icon.
- Like a shape, an icon is a hint for readers: agents implementing a document may ignore it.
- A name the tools don't know is a warning (
W305), and the component is drawn as if it had no icon.
Colors
A component is drawn in its type's color (slate for an untyped one) unless its body names
a color: one of the palette's, by name, or any color as a "#rrggbb" string.
component checkout "Checkout" {
icon shopping_cart
color green
}
component legacy "Legacy Billing" @service {
color "#b45309"
}
- The palette, in the order the editor's picker shows it:
red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate. - A color replaces the type's: the icon is drawn in it, and the component's card or shape takes a tint of it. A product's logo keeps its own colors. Planes have no color.
- Like a shape, a color is a hint for readers: agents implementing a document may
ignore it. Use it to group or highlight (new, deprecated, owned by one team), not
to carry meaning a
descshould state. - A name the language doesn't know, or a string that isn't
#rrggbb, is a warning (W306), and the component is drawn as if it had no color.
3.4 Planes
plane <id> ["Display Name"] [@<type>] [{ <body> }]
A plane is a titled region of a canvas: a background drawn behind the components declared in its body. It's there for readers, to show at a glance what belongs together:
plane frontend "Frontend" {
component web "Web App" @vercel.nextjs
component mobile "Mobile App" @mobile
}
plane cloud "AWS" @aws {
desc "One account, one region."
component api "API" @aws.api_gateway
plane vpc "VPC" @aws.vpc {
component db "Postgres" @aws.rds
}
}
api -> db @sql
mobile -> api @http
web -> api @http
- The body holds, in any mix: a
desc,componentdeclarations (the components on the plane) andplanedeclarations (regions inside it, like the VPC above). Noshape,icon,color,srcor payloads. An empty plane is valid. - A
@typeresolves against the catalog like a component's (§5) and is drawn in the plane's title bar: a cloud account, a region, a network, a cluster. - Transparent to paths. A component on a plane keeps its path —
apianddbabove, notcloud.api— so moving a component onto a plane, off it or to another one never changes a connection. A plane's own path is its scope's path plus its id (vpc), which layout entries and tools use; plane ids share their scope with component ids (E002). - Never an endpoint. Connections join components; one that names a plane is
E101. Connect the components on it or, if the group really is one thing other things talk to, make it a component and nest its parts (§3.1). - Every canvas can have planes. A plane in a component's body is a region on that
component's own canvas, and the components on it are still that component's children
(
orders.apiwhenapisits on a plane insideorders). - The canonical form keeps components in the order they were declared, and writes each plane where its first component comes, with the components on it in its body.
4. Payload blocks
Payloads attach structured or freeform data to a component — or to a connection (§6):
<name> <format> ```
<content>
```
<name>— snake_case identifier, unique per owner (schema,env,endpoints,notes,message, …). Names are free, except the body keywords (component,plane,desc,src,shape,icon,color); they exist so agents and the detail view can refer to a payload by role.<format>— one of:format validation jsonmust parse as JSON yamlmust parse as YAML tablemust parse as a GitHub-flavored Markdown table markdownnone — freeform text Content is fenced with triple backticks. A fence inside content is escaped by using a longer fence on the payload (same rule as Markdown).
Validation failures in
json/yaml/tablepayloads are errors: the document still renders, but is marked invalid and won't compile to an agent export until fixed.Later versions add per-technology validation (e.g.
sqlformat checked as real DDL); this one deliberately does not.
5. Types
A type tag connects a component, plane or connection to the catalog — the curated library
of vendor-neutral building blocks (@load_balancer, @rate_limiter, @cdn, @queue,
@database, …) and real technologies, plus the user's saved custom types.
- Syntax:
@namespace.nameor@name(e.g.@aws.s3,@aws.lambda,@postgres,@vercel,@vercel.functions,@redis,@stripe). - A type resolves against the catalog to an icon, a display style, and (later) detail templates. An unresolved type is a warning, not an error — the component renders generic, and the file still compiles. This keeps files portable between workspaces with different custom libraries.
- Types carry no compatibility rules: nothing constrains what may connect to what.
- Abstract and concrete components mix freely —
component auth "Auth Service"(your own code, untyped or typed with something like@service) can sit besidecomponent cognito @aws.cognito. - Custom types are defined in the app (component library UI) and referenced from files by tag; there are no in-file type definitions yet.
5.1 Initial connection-type library
@http @grpc @graphql @ws @sse @webrtc @tcp @udp @sql @queue @event
@stream @mqtt @webhook @cdc @auth @reads @writes @replicates @triggers
@deploys @contains @depends @mcp @a2a — plus user-defined. All optional.
@mcp is an agent calling an MCP server (Model Context Protocol); @a2a is one agent
calling another (Agent2Agent protocol).
6. Connections
<source-path> <arrow> <target-path> [@<type>] [: "label"] [{ <payload>… }]
Arrows:
| arrow | meaning |
|---|---|
-> |
directed |
<-> |
bidirectional |
-- |
undirected |
Examples:
web -> orders.api @http : "REST, cookie session"
orders.api -> orders.db @sql
orders.api <-> cache @redis_protocol
billing -- stripe
orders.api -> order_events @queue : "order placed" {
message json ```
{ "order_id": "uuid", "total_cents": "int", "placed_at": "timestamp" }
```
}
Rules:
- Both endpoints MUST resolve to declared components. An unresolved endpoint is a
hard validation error (
E101), and so is a plane (§3.4). Nothing is ever auto-created. @typeand label are optional and independent.- Connection payloads. An optional
{ … }body holds payload blocks (§4) describing what travels on the edge: a message schema on a@queue, a request/response shape on an@httpcall, the event names on an@eventbus. The body may contain only payloads — nodesc(use the label), no nested components. Payload names are unique per connection (E003) andjson/yaml/tablecontent is validated (E201), exactly as on components. - Duplicate connections (same endpoints, arrow, type, and label) are a warning. Payloads are not part of a connection's identity: two connections that differ only in payloads are still duplicates.
- Connections between any two components in the tree are allowed, including across nesting boundaries and between a parent and its own descendant. Planes don't affect them.
- Canonical form sorts connections by source path, then target path — so compiled output is deterministic and diffs are clean. Connections carry no sequence/ordering semantics. A connection with payloads is written as a multi-line block separated from its neighbours by a blank line; the sort order is unaffected.
7. The layout block
Everything presentational, in one machine-managed section at the end of the file:
layout {
web: { at: [0, 4], size: [4, 3] }
backend: { at: [6, 0], size: [12, 6] }
api: { at: [1, 2], size: [4, 3] }
api.checkout: { at: [0, 0], size: [4, 3] }
db: { at: [7, 2], size: [4, 3] }
}
- Keys are the paths of components and planes. Values:
at(grid cell,[col, row]) andsize(grid cells). - Every canvas has its own grid: the top level, and the inside of each component
(
api.checkoutabove is placed inside API, not beside it).atis relative to its canvas, or to the plane the item sits on — moving a plane moves what's on it. - Grid-based: all positions are integer grid cells, matching the app's snap-to-grid canvas (React Flow).
- Machine-managed. The canvas writes it; agents only when asked to arrange the diagram. A component or plane with no layout entry — e.g. in a file an agent wrote or that was pasted in — is positioned by the auto-layout engine on load, and its computed position is then written back. A plane grows to hold what's on it.
- Layout is always optional, never load-bearing: deleting the entire block yields the same architecture, auto-laid-out.
- Agent guidance (stated in docs and in the export header): read
layoutas the author's arrangement — context for how they see the system, never something to implement; leave it out when writing (surviving components keep their positions, new ones are auto-placed) unless asked to arrange the diagram.
8. Validation
A document is valid when:
| code | rule | severity |
|---|---|---|
E001 |
file parses (header, declarations, fences) | error |
E002 |
ids unique within their scope, components and planes together | error |
E003 |
payload names unique per component / per connection | error |
E004 |
at most one shape, one icon and one color per component |
error |
E101 |
every connection endpoint resolves to a declared component | error |
E201 |
json / yaml / table payloads parse in their format |
error |
W301 |
type tag resolves in the catalog | warning |
W302 |
duplicate connection | warning |
W303 |
layout entry references a declared component or plane | warning (entry dropped) |
W304 |
shape is one the language knows (§3.3) | warning (drawn as a box) |
W305 |
icon is one the tools know (§3.3) | warning (no icon drawn) |
W306 |
color is a palette name or #rrggbb (§3.3) |
warning (type's color drawn) |
Errors block agent export ("copy as prompt") and mark the document invalid in the UI; the canvas still renders as much as it can. Warnings never block anything.
The canvas enforces E101 interactively: an arrow dropped on empty canvas snaps back —
a dangling connection is never created in the first place.
9. Compilation targets
- Canonical form — the formatter output the app always saves: fixed section order, sorted connections, normalized whitespace. Ensures clean diffs.
- Agent export ("copy as prompt") — canonical form with a complete
layoutblock (every component placed, as the canvas draws it), prefixed by a short fixed header telling the agent what Archer is, how to read it (layout included), and that this document is the implementation spec. One click, ends up on the clipboard. - Image export — PNG of the current canvas view, for pasting alongside the code.
10. Full example
arch "Acme SaaS" v0.2
component web "Next.js App" @vercel.nextjs {
shape browser
desc "App shell + marketing. RSC, deployed on Vercel."
src "apps/web"
src "packages/ui"
}
plane backend "Backend" @vercel {
desc "Everything server-side lives on Vercel."
component api "API" @vercel.functions {
desc "Route handlers. Auth via session cookie."
endpoints table ```
| method | path | purpose |
| ------ | --------------- | ------------------ |
| POST | /api/checkout | create session |
| GET | /api/projects | list projects |
```
component checkout "Checkout" {
icon shopping_cart
desc "Creates Stripe checkout sessions."
}
component projects "Projects" {
icon folders
}
}
component db "Postgres" @supabase.postgres {
shape cylinder
desc "Primary datastore."
schema json ```
{
"users": { "id": "uuid pk", "email": "text unique" },
"projects": { "id": "uuid pk", "owner_id": "uuid fk users.id" }
}
```
}
}
component auth "Auth Service" {
icon lock_key
desc "Our own thin wrapper over Cognito — issues sessions."
}
component cognito "Cognito" @aws.cognito {
shape cloud
}
component stripe "Stripe" @stripe {
shape cloud
}
web -> auth @http : "login"
web -> api @http
auth -> cognito @http
api -> db @sql
api.projects -> db @sql
api.checkout -> stripe @http : "checkout sessions" {
request json ```
{
"price_id": "string",
"success_url": "url",
"cancel_url": "url"
}
```
}
layout {
web: { at: [0, 3], size: [4, 3] }
auth: { at: [0, 8], size: [4, 3] }
backend: { at: [6, 0], size: [12, 6] }
api: { at: [1, 2], size: [4, 3] }
api.checkout: { at: [0, 0], size: [4, 3] }
api.projects: { at: [0, 5], size: [4, 3] }
db: { at: [7, 2], size: [4, 3] }
cognito: { at: [0, 13], size: [4, 3] }
stripe: { at: [20, 3], size: [4, 3] }
}
11. Grammar sketch (EBNF-ish)
file = header { declaration | connection } [ layout ] ;
header = "arch" string version ;
declaration = component | plane ;
component = "component" id [ string ] [ type ] [ "{" { bodyitem } "}" ] ;
bodyitem = shape | icon | color | desc | src | declaration | payload ;
plane = "plane" id [ string ] [ type ] [ "{" { planeitem } "}" ] ;
planeitem = desc | declaration ;
shape = "shape" id ;
icon = "icon" id ;
color = "color" ( id | string ) ;
desc = "desc" ( string | tripleString ) ;
src = "src" string ;
payload = id format fence ;
format = "json" | "yaml" | "table" | "markdown" ;
connection = path arrow path [ type ] [ ":" string ] [ "{" { payload } "}" ] ;
arrow = "->" | "<->" | "--" ;
type = "@" id { "." id } ;
path = id { "." id } ;
layout = "layout" "{" { path ":" layoutval } "}" ;
12. Open questions (deferred)
- Multi-file / import (
use "./billing.arch") — v1 is one file per document. - In-file custom type definitions for portability.
- Per-technology payload validation (
sql,openapi,terraformformats). - Views: multiple named layouts over one model (C4-style level switching).