Topology schema

Every key in the Marrow 2 topology format, with types, defaults, and constraints.

The complete format. A machine-readable JSON Schema is served at /schema.json and can be regenerated with npm run marrow -- schema.

Document

marrow: 2                      # required — the integer 2
name: Acme HQ                  # required
description: One-line summary  # optional — becomes the watermark subtitle
theme: dark                    # dark | light | auto   (default: dark)
hero: core-sw-01               # optional — device featured when the diagram opens
zones: []                      # optional
devices: []                    # the nodes
links: []                      # the cables
KeyTypeRequiredNotes
marrowintegeryesMust be exactly 2. MRW-E003 if missing, MRW-E004 if another value
namestringyesDiagram title
descriptionstringnoWatermark subtitle
themedark | light | autonoDefault dark. auto follows the viewer's OS scheme; a host can also drive any embed's theme by postMessage (see Embedding → Theme)
herodevice idnoCamera focus on open
zoneslistno
deviceslist
linkslist

Identifiers

Every id matches [a-z0-9._-]+, starting with a letter or digit. Device ids are unique among devices; zone ids are unique among zones. Violations are MRW-E012 (invalid) and MRW-E008 (duplicate).

Zones

zones:
  - id: campus            # required, kebab-case
    name: Campus          # optional — defaults to id
    parent: emea          # optional — nest by referencing another zone id
    collapse: auto        # auto | open | closed      (default: auto)
    accent: "#7aa2ff"     # optional hex — tints the zone panel
KeyTypeRequiredNotes
ididentifieryes
namestringnoDefaults to id
parentzone idnoMust exist. Cycles are MRW-E016
collapseauto | open | closednoDefault auto
accenthex colornoe.g. "#7aa2ff"

Devices

devices:
  - id: core-sw-01
    name: Core Switch 1
    icon: cisco/switches/layer-3-switch
    tier: core
    zone: campus
    role: L3 core
    status: up
    mgmt_ip: 10.0.0.1
    ha_pair_with: core-sw-02
    hero: false
    pin: { x: 100, y: 200 }
    accent: "#7aa2ff"
    note: "Change freeze"
    config: "hostname core1"
    interfaces:
      - name: Gi1/0/1
        description: uplink
        vlan: 10
        ip: 10.0.0.1
KeyTypeRequiredNotes
ididentifieryesUnique among devices
namestringnoDefaults to id
iconicon idyesMust exist — MRW-E010 with suggestions if not
tiercore | distribution | access | edge | endpoint | managementnoDrives vertical rank
zonezone idnoMust be declared — MRW-E013
rolestringnoFree text, shown in the inspector
statusup | down | degraded | planned | unknownnoDefault up
mgmt_ipstringno
ha_pair_withdevice idnoDeclare on both devices
herobooleannoAlternative to the document-level hero:
pin{x, y}noFixes position, bypassing placement
accenthex colorno
notestringnoInspector + promotes the device to its own page on sites
configstringnoConfig snippet; same promotion effect
interfaceslistno

Interfaces

KeyTypeNotes
namestringe.g. Gi1/0/1
descriptionstring
vlaninteger1–4094
ipstring

Interfaces are documentation. The port in a link endpoint is geometry. They don't have to match, but agreement makes a much better inspector.

links:
  - from: core-sw-01:Gi1/0/1     # "device-id:Port" — port optional
    to: acc-sw-01:Gi0/48
    kind: fiber                  # the physical medium
    speed: 10G
    mode: trunk                  # the logical function (drives the symbol)
    vlans: "10,20,30"            # VLANs carried on the trunk
    native_vlan: 1
    lag: po1
    lacp: active
    status: up
    animated: true
    label: "×2"
    note: "packet loss 12%"

A link has two independent axes: the physical medium (kind, speed) and the logical function (mode and its refining attributes). They compose — a 10G fiber cable can be a trunk, an access port, or a routed L3 link.

KeyTypeRequiredNotes
fromendpointyesdevice-id or device-id:Port
toendpointyesSame
kindcopper | fiber | dac | wireless | virtual | wan | serialnoPhysical medium
speed10M | 100M | 1G | 2.5G | 5G | 10G | 25G | 40G | 100G | 400GnoDrives stroke width
modeaccess | trunk | routed | tunnel | peering | stacknoLogical function — drives the symbol (see below)
vlansstring | integernoVLAN set/range on a trunk, or the single VLAN on an access port, e.g. "10,20,30-39"
native_vlaninteger 1–4094noNative (untagged) VLAN on a trunk
encapipsec | gre | vxlan | wireguard | dmvpn | mpls | l2tp | geneve | sslnoOverlay/VPN encapsulation on a tunnel
protocolospf | ebgp | ibgp | bgp | eigrp | isis | rip | staticnoRouting/control-plane protocol on a routed or peering link
lagstringnoBundle id — links sharing it between the same pair aggregate onto one strand
lacpactive | passive | on | static | pagpnoAggregation control mode; pairs with lag
statusup | down | degraded | plannednoDefault up. No unknown on links
animatedbooleannoFlowing dash — use sparingly
labelstringnoEdge label (aggregate bundles auto-label ×N)
notestringno

mode declares what the cable carries; the compiler renders each as a distinct symbol (see ADR-0009). It is declared, never inferred — the engine will not guess trunk from a port name. The refining attributes read like device config:

ModeSymbolCompanion keysReads like
accessplain line, v<vlan> chipvlansan access switchport
trunkdouble rail + VLAN chipvlans, native_vlanswitchport mode trunk
routedsolid line + protocol chipprotocolan L3 routed interface
peeringdotted line + protocol chipprotocolan OSPF/BGP adjacency
tunneldashed line + 🔒 encap chipencapa site-to-site VPN / overlay
stackthick bar, stack chipa stack/backplane cable

Aggregation is orthogonal to mode: give the members a shared lag id (plus an optional lacp mode) and they bundle onto one strand whose chip is the bundle id (PO1). A LAG can be a trunk, an access, or a routed port-channel.

# an LACP port-channel carrying a trunk
- { from: core-1:Te1/0/1, to: core-2:Te1/0/1, lag: po1, lacp: active, mode: trunk }
- { from: core-1:Te1/0/2, to: core-2:Te1/0/2, lag: po1, lacp: active, mode: trunk }
# a site-to-site IPsec VPN and a VXLAN overlay
- { from: fw-a:tun0, to: fw-b:tun0, mode: tunnel, encap: ipsec }
- { from: leaf-1, to: leaf-2, mode: tunnel, encap: vxlan }
# eBGP peering to an upstream
- { from: edge-rt, to: isp-rt, mode: peering, protocol: ebgp, kind: wan }

Attributes that don't match the mode are advisory warnings, not errors (MRW-W105) — e.g. encap on a non-tunnel link, or lacp without a lag.

Endpoint parsing

Only the first colon splits device from port.

core-sw-01:Gi1/0/1   →  device "core-sw-01", port "Gi1/0/1"
core-sw-01           →  device "core-sw-01", no port

Since ids are kebab-case, they never contain a colon, so this is unambiguous. A malformed endpoint is MRW-E014; an unresolvable device is MRW-E009.

Referential rules

All enforced by validate:

  • Every zone and device reference must exist.
  • Ids are unique within their kind.
  • A port reused across links → warning MRW-W101.
  • An unlinked device → warning MRW-W103.
  • An empty zone → warning MRW-W102.
  • A link from a device to itself → warning MRW-W104.
  • A zone hierarchy cycle → error MRW-E016.

Warnings are advisory — usually worth fixing, never blocking.

Private annotations

devices:
  - id: core-sw-01
    icon: cisco/switches/layer-3-switch
    x-owner: netops
    x-cmdb-id: CI0004421

Any x- prefixed key at any level is preserved in the file and ignored by the compiler.

Inert legacy keys

x-v1-config: may appear in files produced by marrow import. It records the archived v1 configuration (engines:, layouts:, output_filename:) and has zero effect in v2. Delete it.