The DOT format
The smallest valid input
digraph { core -> access }
Two devices, one link, two tiers. Everything else on this page is optional detail layered onto that. The input is standard Graphviz DOT — the graph, not the styling: Drawbridge reads the topology and the data attributes, redraws everything with its own shapes and routing, and ignores decoration (fontname=, layout=, named colors). One presentation attribute IS read: pos= — when every device carries one, as in Drawbridge's own export or dot -Tdot output, the drawing keeps that layout instead of being laid out afresh (the DOT reference page has the exact rules). A file written for Graphviz imports here; a file written for Drawbridge still renders in any Graphviz tool.
Ids, labels and quoting
The id is what edges wire against; the label is what the tile shows. When a label= is present it becomes the device's display name; without one, the id is shown.
digraph {
fw1 [kind=firewall, label="FW-EDGE-01"]
sw1 [kind=switch, label="SW-CORE-01"]
fw1 -> sw1
}
Line breaks. DOT writes a break as \n inside the quoted string, and that is what you get: label="SW-CORE-01\nfloor 2" imports as a two-line name. \l and \r (Graphviz's left- and right-justified breaks) break the line too — the justification is presentation and is dropped, the break is not. \\ is a literal backslash, and \N stays as written: it is Graphviz's node-name placeholder, not text.
Quoting is the one DOT rule that bites: an unquoted id or value may contain only letters, digits and underscores. Anything with a hyphen, dot, space or slash must be quoted — label="SW-CORE-01", kind="l3-switch", "10.0.0.1". An unquoted kind=l3-switch is not "an attribute Drawbridge rejects", it is invalid DOT, and the whole block is reported as unreadable.
Arrows decide the tiers
The layout is layered, top-down: whatever arrows point away from sits above whatever they point at. Write edges downstream — internet and core at the tail, access at the head — and the tiers fall out with no extra syntax:
digraph {
inet [kind=internet]
fw1 [kind=firewall]
core [kind="l3-switch"]
acc1 [kind=switch]
acc2 [kind=switch]
inet -> fw1
fw1 -> core
core -> {acc1 acc2}
}
Three forms all work and mix freely: single edges (a -> b), chains (a -> b -> c — every hop a link), and brace fan-outs (core -> {acc1 acc2} — one link per member). An undirected graph (graph { a -- b }) imports too; with no arrow to read, tiers follow write order.
Same-tier peers — HA pairs and MCLAG
Two devices that are peers — an HA firewall pair, MCLAG distribution switches — must not stack, even though a link runs between them. Declare the tier with a multi-node rank=same group and link them normally:
digraph {
fw1 [kind=firewall, label="FW-A"]
fw2 [kind=firewall, label="FW-B"]
core [kind="l3-switch"]
{rank=same; fw1 fw2}
fw1 -> fw2
fw1 -> core
fw2 -> core
}
The peer link is drawn and routed like any other, but it is kept out of the layering, so fw1 and fw2 share a row instead of one being pushed a tier below the other. A one-node rank=same group (which the FortiGate emits) is harmless and means nothing.
One nuance: the declaration removes the peer link from the layering — the peers' other links still decide their rows. Give both members uplinks at the same depth (as above, where both firewalls hang off inet) and they land level; a peer whose only uplink arrives from a deeper device will sit on that deeper row regardless of the declaration.
Device attributes
digraph {
fw1 [kind=firewall, label="FW-EDGE-01", mgmtIp="10.0.0.1",
serial="FG100FTK00000001", model="FortiGate-100F",
location="DC1 rack 3", notes="HA primary", color="#e11d48"]
}
kind=picks the shape: any palette shape name (firewall,switch,"l3-switch",server,internet,database,plc, …) or the palette's display name (kind="Wi-Fi AP",kind="L3 switch"). The DOT reference page lists all of them. An unknown kind imports as a neutral shape and is reported — a wrong icon would look deliberate, a neutral one asks to be fixed.mgmtIp=,serial=,model=,location=,notes=fill the inspector's asset fields, which also flow into the CSV inventory export.color=sets the tile accent — hex values only ("#e11d48"). Named colors are ignored on purpose: Graphviz files decorate withcolor=grey/redeverywhere (the FortiLink dump does), and honoring those would repaint every import. A hex value is somebody steering this app deliberately.
When the id itself is a Fortinet serial (FG100FTK…, S448DFTF…), the serial lands in the asset fields automatically, and a FortiGate/FortiWiFi model is derived from it where the serial encodes one.
Link attributes
digraph {
fw1 [kind=firewall]
sw1 [kind="l3-switch"]
fw1:port1 -> sw1:ge1 [speed="10G", link=trunk, label="uplink", color="#0284c7"]
}
- Ports become port labels.
fw1:port1 -> sw1:ge1labels each end with its interface.taillabel=/headlabel=do the same thing and win over the port form when both are present. - Sides. The cardinal compass points attach a link to a side of the tile:
fw1:e -> fw2:wleaves fw1 on the east side and arrives on fw2's west.fromdir=/todir=(n,s,e,w) are the attribute spelling. Diagonals (:ne,:sw) have no side to map to and are reported and ignored. speed="10G"is the speed tag;label=the caption;link=the connector type (ipsec,trunk,lag,wan,mgmt,wireless,conduit,fieldbus— each with its line style);color=the stroke, hex only.- Aggregates. Links sharing a
bundle=name are drawn as one bundle ring — a LAG, an HA pair's bond. A cable can be in several at once (its leaf's port-channel and its spine's, say): separate the names with spaces,bundle="po1 spine_a".
digraph {
sw1 [kind=switch]
srv [kind=server]
spine1 [kind="l3-switch"]
sw1:ge1 -> srv:eth0 [bundle=po1]
sw1:ge2 -> srv:eth1 [bundle=po1]
sw1:ge9 -> spine1:et1 [bundle="po1 spine_a"]
}
Real-world pastes: chrome, VDOMs, defaults
The importer is built for output that was never meant to be clean:
- CLI chrome is fine. The FortiGate wraps its digraph in
Managed FortiLinks in current VDOM root: …— paste everything; text outside thedigraph { … }blocks is skipped silently. - Several digraphs in one paste (a multi-VDOM dump) all import, and a device named in more than one block is merged into one tile.
- Positional defaults are honoured.
node [fillcolor = red]applies to devices that first appear after it — which is exactly how the FortiLink dump encodes roles (red = the FortiGate, blue = tier 1, green = tier 2+). Comments (// tier 1,/* … */) are read past. - Named switches import. Give your FortiSwitches hostnames and the FortiGate prints those instead of serials —
10405-001-sw02rather thanS148FFTF…. A name like that is not legal Graphviz (it starts with a digit and carries hyphens, sodotitself will not read the file), and the dump separatesrank=samemembers with commas, which Graphviz allows but the letter of the format does not. Both are repaired as the paste is read, so the dump goes in exactly as your terminal printed it. A named switch has no serial to harvest, so its asset fields start empty — the roles still come from the fillcolors.
What gets reported, and what gets refused
The policy is import the good rows, report the rest — real pastes always carry legends, prompts and truncated scrollback, and refusing everything over one bad line would fail on exactly the input this feature exists for. The preview shows every reported line with its line number before anything is committed:
- skipped — the line produced nothing: an unreadable digraph block, a device linking to itself.
- adjusted — imported, but not exactly as written: an unknown
kind=(imported as neutral), an unknownlink=, a diagonal compass point,rankdir=LR(drawn top-down regardless), asubgraph cluster_…without akind=orpurdue=declaration (its devices import; the grouping does not), a repeated id-less edge line (the duplicate is dropped — give parallel cables distinctid=values).
One thing is a hard refusal: a block that is not valid DOT at all — a quote that never closes, an arrow with nothing after it, an attribute list that never ends. The report names the line:
Line 3: a -> } — not readable as DOT — Expected "<", "\"", "{", NUMBER …
Unquoted names are not in that category, even though strict DOT says they should be. 10405-001-sw02, l3-switch, rack-4.2 — a name that leads with a digit, or carries a hyphen or a dot — are all things Graphviz itself refuses to read, and all things a switch is genuinely called. They are quoted for you on the way in. So is a comma where DOT wants a semicolon ({rank=same; sw01, sw02}). You never have to tidy a paste up before it will import.
Purdue levels
One container DOES ride the import: declare Purdue levels as clusters and the backdrop draws itself —
digraph {
subgraph cluster_l3 {
purdue="3"
scada [kind="scada-server"]
}
subgraph cluster_l2 {
purdue="2"
hmi1 [kind=hmi]
}
scada -> hmi1
}
The import places the palette's Purdue-levels underlay behind the topology, sized so every declared band fits its devices, and seats each cluster's members inside band N (5, 4, 3.5, 3, 2, 1, 0 — the model's own numbering; anything else is reported). Devices outside any declared level keep their laid-out position. The DOT examples page has the full reference architecture.
The way back out
Export → Graphviz DOT (or Copy DOT) writes the page back out in this same profile — layout included — so the loop closes: text in, drawing out, text again, and importing your own export puts every tile back exactly where it was. A page with an HA pair over a core switch comes back as the file it would have been imported from —
digraph Core {
inputscale=72
subgraph cluster_1 {
kind=zone
label=DMZ
cidr="10.20.30.0/24"
bb="120,480,600,720"
"FW-CORE-01" [kind=firewall, mgmtIp="10.20.30.1", pos="264,560!", width="1.333333", height="1.111111"]
"FW-CORE-02" [kind=firewall, pos="440,560!", width="1.333333", height="1.111111"]
}
"SW-CORE-01" [kind="l3-switch", pos="352,240!", width="1.333333", height="1.111111"]
{rank=same; "FW-CORE-01" "FW-CORE-02"}
"FW-CORE-01" -> "FW-CORE-02" [id=e1, label=HA]
"FW-CORE-01" -> "SW-CORE-01" [id=e2, taillabel=port1, headlabel=ge1, link=trunk, speed="10G"]
}
pos= is Graphviz's own attribute — the centre, in points, y growing up — and inputscale=72 is what lets plain neato render this file with your geometry, so the export stays ordinary Graphviz. Every edge carries its id=: that is what keeps the two cables of an HA pair two cables on the way back in, when nothing else tells the identical lines apart. Two more things are written for you rather than copied. Arrows come out pointing down the page — the device drawn higher is at the tail — because that is what decides tiers when a file has no positions, and two devices sharing a name are numbered (Switch, Switch 2), because a name is what identifies a device here and identical names would merge into one tile. The details, and what a zone does or does not survive as, are in Export under Graphviz DOT.
Zones, racks and virtual contexts
A cluster that names its kind is a real container: subgraph cluster_x { kind=zone label="DMZ" cidr="10.1.2.0/24" … } imports as a drawn zone holding whatever stands on it — with its subnet in the title, at its recorded rect when the file carries one (bb=, which the export writes), or wrapped around its members with comfortable padding when it does not. kind=rack and kind=vcontext work the same way. A cluster without a kind still imports flat (its devices come in, the box does not) — that is what a grouping in a foreign Graphviz file usually means.
Not yet importable
Maps (country and world backdrops) cannot arrive through the import — add them in the editor afterwards; imported devices drop below existing content and never disturb what is already drawn. They are reported in the preview when a paste contains them, so nothing disappears silently.