Code in, image out

The whole loop in one command

The desktop app doubles as a command-line renderer: give it topology text, get a finished image back — layout, icons and routing included, no window, no clicks.

Drawbridge --import topo.gv --export topo.svg

--export accepts seven extensions, and the extension picks the format:

Output What you get
.svg The drawing as a vector — crisp at any size, ideal for wikis and docs
.png A 2× raster of the same drawing
.drawio The fully editable document — open it in Drawbridge later and polish
.dot The laid-out diagram back as Graphviz DOT, in the profile the importer reads
.csv The cable schedule — one row per cable, both ends with their ports, speed, cable type, label
.netbox.csv The same cabling in NetBox's cable bulk-import columns, ready for its import form
.describe.json The document as structured JSON — devices, links, pages, layers, rack positions — for scripts and agents that read drawings instead of making them

The extension is matched by its longest suffix, so plan.netbox.csv is the NetBox import and plan.csv is the readable schedule — two files that share .csv and must not collide. The two cabling files are the same ones the Export menu writes, so a pipeline and a person get identical output; Cable schedule CSV, and NetBox in the Export chapter describes the columns.

The input is anything Import accepts — Graphviz DOT (with the full attribute profile), a FortiLink dump with its CLI chrome, CDP/LLDP output, a CSV. --import - reads standard input, which is what makes pipes work:

ssh fgt 'execute switch-controller get-physical-conn dot fortilink' \
  | Drawbridge --import - --export topology.png

That one line is the function this chapter exists for: live gear in, presentation-ready image out, no human in the loop.

Reading a drawing instead of making one

--doc takes a finished .drawio file as the input — the question it answers is not "draw this" but "what is in this drawing":

Drawbridge --doc network.drawio --export audit.describe.json

The JSON lists every page with its devices (id, kind, label, layer, asset data, rack position with its bottom-up U), every cable (both ends with ports, speed, type, medium, aggregates, installed tick) and the page's layers — the same vocabulary as the cable schedule, without the geometry. --doc - reads the document from standard input like --import - does. It pairs with jq the way the import loop pairs with ssh:

# every device that has no management IP on file
Drawbridge --doc network.drawio --export audit.describe.json
jq -r '.pages[].devices[] | select(.data.mgmtIp == null) | .label' audit.describe.json

describe.json also works after --import: --import topo.gv --export topo.describe.json imports the topology and answers with the structured listing instead of a picture — useful when the DOT was generated and you want to check what the importer actually understood.

And --doc is not limited to describing: every export in the table works from a document, so --doc network.drawio --export network.png re-renders a finished drawing headlessly — regenerate the wiki image after an edit, produce the cable schedule from a file a colleague sent, pin --theme light so the output never follows the machine's appearance.

The local HTTP API

For pipelines that would rather speak HTTP than spawn a process per file, Drawbridge --serve runs the same capabilities as a local web API:

Drawbridge --serve            # http://127.0.0.1:8722
Drawbridge --serve --port 9000
curl -X POST --data-binary @network.drawio http://127.0.0.1:8722/api/describe
curl -X POST --data-binary @network.drawio "http://127.0.0.1:8722/api/export/png?theme=light" -o network.png
curl -X POST --data-binary @topo.gv "http://127.0.0.1:8722/api/diagrams/svg?theme=light" -o topo.svg

POST /api/describe answers with the structured JSON; POST /api/export/{kind} renders a document to any format in the table; POST /api/diagrams/{kind} imports topology text and answers with the export — the importer's notes ride in the x-drawbridge-notes response header as a JSON array. The full surface is described by OpenAPI at GET /api/openapi.json, so client generators and API tooling can consume it directly.

The listener binds 127.0.0.1 only and carries no authentication — it is for the machine Drawbridge runs on, the HTTP twin of the CLI. Requests carrying an Origin header are refused, so a web page cannot drive it from inside your browser.

The hosted API

The same routes run on our server, so a pipeline that cannot install anything still gets a rendered diagram back:

curl -X POST --data-binary @topo.gv \
  "https://drawbridge.fortiknight.com/api/diagrams/svg?theme=light" -o topo.svg

Same paths, same formats, same x-drawbridge-notes and x-drawbridge-name response headers — swap the origin and a local recipe works unchanged. No account and no key: 2000 elements per drawing, 5 MB per request, a render killed at 60 seconds, and no per-caller quota at all, so a burst answers 429 with Retry-After: 5 instead of closing the door for an hour. Documents are rendered, returned and forgotten; nothing is stored. GET /api/openapi.json describes the whole surface there too.

It is a beta on a small fleet, so treat it as best effort — and if what you are driving is an AI assistant rather than a script, use the MCP door on the same host instead: it is one line of configuration and the tools tell the assistant how to draw well. See AI assistants (MCP).

Where the command lives

Drawbridge above stands for the installed binary; the path differs per platform:

Platform Command
macOS /Applications/Drawbridge.app/Contents/MacOS/Drawbridge
Linux (.deb) /opt/Drawbridge/drawbridge
Linux (AppImage) ./Drawbridge-<version>-x86_64.AppImage
Windows (installed) "%LOCALAPPDATA%\Programs\Drawbridge\Drawbridge.exe"
Windows (portable) Drawbridge-Portable-<version>-x64.exe

A shell alias keeps recipes readable: alias drawbridge='/Applications/Drawbridge.app/Contents/MacOS/Drawbridge'.

The contract, for scripts

Without --export, --import opens the app normally with the Import box pre-filled — the halfway point when you want the preview before committing:

ssh fgt 'execute switch-controller get-physical-conn dot fortilink' | Drawbridge --import -

Recipes

A nightly topology snapshot. Cron on any machine with the app installed:

#!/bin/sh
ssh fgt 'execute switch-controller get-physical-conn dot fortilink' \
  | /opt/Drawbridge/drawbridge --import - --export "/srv/wiki/topology-$(date +%F).png"

Diagram-as-code in a repository. Commit network.gv next to your docs and regenerate the image whenever it changes:

drawbridge --import network.gv --export docs/network.svg
git add network.gv docs/network.svg

The .gv file is the reviewable source — a pull request shows the topology change as a readable text diff, and the SVG is its rendering.

Generate the DOT itself from data. Anything that can print text can feed the pipe — an inventory, IPAM, a CMDB export:

node -e '
  const hosts = [
    ["fw1", "firewall", "FW-EDGE"],
    ["sw1", "switch", "SW-CORE"],
    ["srv1", "server", "SRV-01"],
  ];
  console.log("digraph {");
  for (const [id, kind, label] of hosts) {
    const k = JSON.stringify(kind);
    const l = JSON.stringify(label);
    console.log(`  ${id} [kind=${k}, label=${l}]`);
  }
  console.log("  fw1 -> sw1 -> srv1");
  console.log("}");
' | drawbridge --import - --export inventory.png

Batch a directory of topologies:

for f in topologies/*.gv; do
  drawbridge --import "$f" --export "out/$(basename "${f%.gv}").svg"
done

Keep the editable file too. Export .drawio alongside the image and the generated diagram stays a living document — open it, add zones and notes, save:

drawbridge --import topo.gv --export site-a.drawio

The cabling, without opening the app. The same run can produce the patching list beside the picture — one command per file:

drawbridge --import topo.gv --export docs/topology.svg
drawbridge --import topo.gv --export cables.csv
drawbridge --import topo.gv --export cables.netbox.csv

An AI in the pipe. Any tool that can call an AI and capture text can complete the loop with no human step: prompt (the one on this chapter's first page) → DOT → drawbridge --import - --export out.svg. The DOT examples page shows what the intermediate text looks like.

In CI — GitHub Actions

Everything above works on a headless runner; two things are specific to CI and both are handled in the snippets below. Electron needs a display even when it never shows a window — xvfb-run provides a virtual one — and in containers running as root it needs --no-sandbox. This exact recipe (virtual display, --no-sandbox, the .deb install, the import→SVG run) is what Drawbridge's own release verification executes in a clean Ubuntu container, so it is tested machinery, not a sketch.

Re-render the map whenever the topology file changes. Commit network.gv to the repository; every change to it publishes a fresh SVG as a build artifact:

name: network-map
on:
  push:
    paths: ['network.gv']
jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Drawbridge and a virtual display
        run: |
          url=$(curl -fsS https://drawbridge.fortiknight.com/download/manifest.json \
            | jq -r '.versions[0].files[] | select(.platform=="linux-x64-deb").url')
          curl -fsSLo drawbridge.deb "https://drawbridge.fortiknight.com$url"
          sudo apt-get update
          sudo apt-get install -y xvfb ./drawbridge.deb
      - name: Render
        run: |
          xvfb-run -a /opt/Drawbridge/drawbridge --no-sandbox \
            --import network.gv --export network.svg
      - uses: actions/upload-artifact@v4
        with:
          name: network-map
          path: network.svg

The download step asks the live manifest for the current release, so the pipeline tracks Drawbridge versions on its own; pin a version instead by hard-coding /download/files/vX.Y.Z/… from the same manifest.

A nightly map of the live network. The same job on a schedule, pulling the topology off the FortiGate over SSH and committing the rendered map back — documentation that redraws itself:

name: live-topology
on:
  schedule:
    - cron: '17 5 * * *'
  workflow_dispatch:
jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Drawbridge and a virtual display
        run: |
          url=$(curl -fsS https://drawbridge.fortiknight.com/download/manifest.json \
            | jq -r '.versions[0].files[] | select(.platform=="linux-x64-deb").url')
          curl -fsSLo drawbridge.deb "https://drawbridge.fortiknight.com$url"
          sudo apt-get update
          sudo apt-get install -y xvfb ./drawbridge.deb
      - name: Pull the live topology
        env:
          SSH_KEY: ${{ secrets.FGT_SSH_KEY }}
        run: |
          install -m 600 /dev/null key && printf '%s' "$SSH_KEY" > key
          ssh -i key -o StrictHostKeyChecking=accept-new admin@fgt.example.net \
            'execute switch-controller get-physical-conn dot fortilink' > topology.gv
      - name: Render and commit
        run: |
          xvfb-run -a /opt/Drawbridge/drawbridge --no-sandbox \
            --import topology.gv --export docs/topology.svg
          git config user.name "topology-bot"
          git config user.email "topology-bot@users.noreply.github.com"
          git add docs/topology.svg topology.gv
          git diff --cached --quiet || git commit -m "docs: refresh network topology"
          git push

Notes for other CI systems: the pattern is identical — install the .deb (or unpack the AppImage), wrap the call in xvfb-run -a, add --no-sandbox when the job runs as root, and read the exit code. Everything the run reports arrives on stdout/stderr exactly as described in the contract above, so a red pipeline step means a real import or export failure, with the reason in the log.

The CLI needs the desktop app on the machine running it. When the recipient is a person with a browser, build an import link instead (see the chapter's first page): the diagram travels inside the URL, their browser does the rendering, and nothing is installed anywhere. CLI for machines and pipelines; links for people. When the person is the one running the cables, the cable schedule has a link of its own — Share in the schedule panel, described in the Connectors chapter.