File format¶
A .grafli file is line-oriented plain text. One element per line. Lines
that start with # are comments. Order does not affect rendering — the
layout is encoded in each element's coordinates.
Header¶
The first line is a shebang-style header that identifies the format and its version. It is required.
Element types¶
@ box <id> "<label>" <x>,<y> <w>x<h> [color] [^anchor] [~size] [!style] [>parent]
@ note [<id>] <x>,<y> "<text>" [color] [~size] [~width=N] [!style] [>parent]
@ arrow <from> <op> <to> ["label"] [color] [!pattern] [!thickness] [!routing] [~size]
@ image <id> "<relative_path>" <x>,<y> <w>x<h> [!frame|!noframe] [>parent] [&attach]
| Element | Purpose |
|---|---|
box |
Rectangular container with a label. Can nest via >parent. |
note |
Free-form text block. Supports tasks, questions, code-mode, Markdown-mode, and discussions (see Text annotations). |
arrow |
Directed/bidirectional connector between two elements. |
image |
A picture placed on the board (see Images). |
The image path is resolved relative to the directory of the .grafli file,
so a board and its <stem>-res/ directory move together. Raster formats
(PNG, JPEG, …) and .svg both work; an SVG is rendered from the file at
paint time, so it stays sharp at any zoom and an external edit of the file
shows up on the board. By default a raster image is painted with a subtle
border and an .svg without one; !frame / !noframe override that per
image and are serialized only when they deviate from the default.
Arrow operators¶
| Operator | Meaning |
|---|---|
-> |
Right (from → to) |
<- |
Left (from ← to) |
<-> |
Bidirectional |
-- |
No arrowhead |
Modifiers¶
<color>— built-in tokens:%base,%primary,%secondary,%tertiary,%subtle,%accent,%highlight,%muted,%soft,%clay,%teal,%rose,%forest,%plum, or any#rrggbbhex value. Tokens are semantic: each one resolves to a different value in the light and dark theme, so a board written by someone on light reads correctly on dark. Prefer them over hex, which is kept exactly as written and so only suits one theme.^anchor—topleft,top,topright,left,center,right,bottomleft,bottom,bottomright. Controls how a box's label is placed.~size—xsmall,small,medium,large,xlarge.~width=N— (notes only) override the wrap width in characters. Notes auto-wrap to 80 chars by default so long AI-generated lines stay readable; this modifier sets a different budget per note (e.g.~width=40for a narrow caption,~width=120for a wide code listing). You can also drag the right edge of a selected note to set this interactively — the value persists on save.!style—flat,dashed, plus arrow-specific styles. Notes render in a handwritten face by default;!mono(andcode:notes) switch to the monospace face — handwriting for prose, monospace for code. (Style mode →topens the text grid;Tabthere toggles a note's font.)!routing(arrows) — how the connector travels between its ends: omitted for a direct line (the default),!splinefor a curve,!orthofor a right-angle stair. Independent of!patternand!thickness, so!dashed !orthois a dashed stair. A routed connector leaves each box perpendicular to the side that faces the other end, and several connectors sharing a side are spread along it automatically — anchors are always derived from the board, never written into the file, so a board renders the same in the app and ingrafli render. A stair also slides its turn clear of boxes standing in the gap, but it never detours around them: the whole route stays inside the span between its two ends, so moving a box shifts a stair predictably instead of sending it off on a new path. A connector that still cuts across your layout is a layout decision, andgrafli diagnoseis the place to catch it.- (arrows) a bare
%color/#hexoverrides the connector colour;!dashed/!dottedset the line pattern and!thin/!thickset the thickness (default width tracks the linked nodes). Select a connector (or shift+click several) and presssthencfor a live overlay over all four axes — heads, line, thickness, colour — orsthentfor the label text; the picks apply to every selected connector at once. !bold/!italic— (boxes and notes) text emphasis layered on the size, e.g.~large !boldfor a heading. Combine freely (!bold !italic). (Style mode →topens a size × style text grid.)*symbol— (boxes and notes) attach a sketchnote symbol. Three placements: bare*nameis fill (a big symbol with the label/text as a caption — a framed node on a box, a borderless marker on a note);*lead:nameis lead (a small symbol left of the label, which stays primary —*lead:lock→ 🔒 Auth);*badge:nameis badge (a compact overlay in the top-right corner, the label keeps its normal layout — for emphasis marks on existing nodes). Digits are number badges:*3,*badge:7(1–99) render as a circled number for sequences and rankings. The symbols live ingrafli/assets/sketchnote_symbols.svg— one editable sheet, rendered vector-crisp at any zoom. Semantic names (what a thing is):person,robot,gear,database,document,cloud,globe,target,lightbulb,question,warning,check,cross,flag,clock,calendar,magnifier,puzzle,lock,plant,money,link. Emphasis names (how much it matters):star,heart,flame,exclamation,brain,lightning,repeat,exercise,performance. Legacybulb/docstill parse as aliases oflightbulb/document. (Style mode →iopens the symbol grid;Tabcycles fill → lead → badge;1–9types a number badge.)>parent— nest this element inside the box with the given ID.
When to reach for symbols and emphasis. They shine when you're explaining a concept — mind maps, idea boards, walkthroughs — where a
*lightbulbnode or a bold heading aids recognition. On structural diagrams (state machines, architecture, data flow) keep it clean: boxes, labels, arrows, and one colour per category read as a system; symbols and bold are mostly noise there. Default to restraint.
Graph connectors are drawn with a thickness proportional to the size of the nodes they link — big containers get heavier arrows, small inner children stay light — so a zoomed-out view reads as a clear hierarchy. This is automatic; the weight is capped by the smaller of the two endpoints.
Block text¶
When a note's text contains quote characters or spans multiple lines, use triple quotes:
@ note logic 100,320 """
code:
handleRequest(req) -> Response
call validate(req)
emit RequestAccepted(req.id)
return ok
"""
The serializer auto-promotes single-line notes that contain " to the
triple-quoted form.
Quoted-text escapes¶
Single-line quoted slots (box / arrow / bookmark / flow labels,
descriptions, note text, the footer) support two escapes: \n for a
newline and \" for a literal quote — @ box a "Say \"hi\"" 0,0 200x100.
Triple-quoted blocks take quotes and newlines literally.
A complete example¶
#!grafli v1
# A small architecture sketch
@ box frontend "Frontend" 100,100 160x60 %secondary
@ box backend "Backend" 320,100 160x60 %primary
@ box db "Database" 320,240 160x60 %subtle
@ arrow frontend -> backend "call: REST API"
@ arrow backend -> db "data: queries" !dashed
@ note 100,240 "SPA with React"
Bookmarks and flows (v2)¶
Saved viewpoints and guided tours are stored in the file too. A file that
contains them uses the v2 header; pure-diagram files stay on v1.
@ bookmark <id> "<label>" @<focus_id>[,<focus_id>...] [~pad=<n>] [~iso] ["<description>"]
@ bookmark <id> "<label>" ~view=<x>,<y>,<w>,<h> ["<description>"]
@ flow <id> "<label>" <step> ... [~auto=<start_id>] [~detail=<v>] [~focus=<v>] ["<description>"]
@ footer "<markdown>"
@ title-bg <empty|thumbnail-art>
A flow <step> is a bookmark ref with optional :-separated segments, in
any order: a bare number is the auto-play dwell in seconds, and
detail=<v> / focus=<v> override the flow's presentation settings for
just that stop — bm_all:6:detail=summary or bm_api:focus=complete.
@<ids>is the semantic anchor — the item ids the view frames. The pan/zoom is computed by fitting them, so the bookmark survives layout edits.~pad=<n>overrides the framing padding.~isomarks the anchor as a narrowed selection: thumbnails and exported slides render only the anchored items (and the arrows between them), not everything inside the framed region — see scoping a step.~view=<x>,<y>,<w>,<h>stores an exact scene rect instead, used for a hand-tuned framing or a viewpoint that contains no nodes. A bookmark uses one or the other.- A
@ flowlists bookmark ids in order;:<dwell>sets that stop's auto-play time in seconds (omit for the flow default).~auto=<start_id>marks an auto-generated flow, regenerable from that start node. ~detail=<v>sets how the flow's stops render under level-of-detail:full(everything as authored),summary(containers collapse to tiles), orauto(follow the app's global LoD toggle — the default when unset). A step's owndetail=segment overrides the flow.~focus=<v>controls distraction fading:completeshows only elements completely inside the framed viewport at full opacity and blends out partially visible ones (a connector stays opaque only when both its ends do);none(default) shows the frame as-is. A step'sfocus=segment overrides the flow. See per-stop detail & focus.- A bookmark description is the playback / slide caption: it renders in
full (wrapped), so keep it within 280 characters — the GUI editor
enforces the budget and
grafli export --checkflags older files past it. @ footeris the board-global markdown branding line on exported content slides;@ title-bg thumbnail-artselects the title slide's collage backdrop. Both are one-per-file.
#!grafli v2
@ box api "API Gateway" 280,0 180x80 %secondary
@ box auth "Auth Service" 280,160 180x80 %soft
@ bookmark bm_auth "Authentication" @api,auth "Verified before routing."
@ flow tour "Walkthrough" bm_auth:6 "A short guided tour."
See Bookmarks & flows for capturing, editing, playback, present mode, and PDF export.
Why plain text¶
- Git-native — every change is a line-level diff with intent baked in.
- Editor-friendly — open with any text editor; grafli watches the file and reloads automatically.
- AI-ready — the format is small enough that LLMs reliably read, modify, and emit valid diagrams from natural language.