md2hd

Reference

Markdown reference

Every card field, link form, map setting, fallback, normalization rule, and silent failure md2hd understands.

A map is one or more markdown files. Each YAML frontmatter block becomes a node, shown as a card. Relationships and wikilinks become edges, shown as connections. Prose after a block becomes that node’s note.

The parser does not reject invalid input. A malformed block or misspelled setting still produces a result, often the wrong one. Use the silent-failure table when the map draws differently from what you wrote.

File structure

One file may contain the entire map:

---
type: map
title: Partnerships
---

---
id: riverside-council
type: organization
title: Riverside City Council
---

The note for this card.

---
id: dana-whitfield
type: person
title: Dana Whitfield
---

The note for Dana.

A line containing --- opens a block only when the next non-blank line looks like a YAML key. The key must start with a letter or underscore and have a space or line ending after its colon.

Next lineOpens a block?
id: acmeYes
rel:Yes
_id: acmeYes
id:acmeNo
2026: noteNo

Prose before the first block is dropped. A --- followed by prose stays inside the current note as a horizontal rule. *** and ___ never open blocks.

Node fields

All fields are optional, but an explicit id, type, and title avoid surprising fallbacks.

---
id: acme
type: organization
title: Acme Corp
subtitle: Municipal · 9 members
description: The account at the center of this map.
weight: lead
tags: [prospect, warm]
---

Everything after the block is the note. Markdown renders in the card panel.
FieldEffect
typeCard category. Defaults to note. map is reserved for configuration.
idLink target. Defaults to a slug of the title, then a file-and-block fallback.
title / nameCard name. Defaults to the first heading anywhere in the note, then Untitled.
subtitleOne line under the card title.
descriptionOpening paragraph in the card panel.
weightVisual emphasis from 1 to 5.
tagsChips on the card and in the panel. Nested arrays are flattened.
rel / rels / linksNamed connections.
Any other keyA wikilink value becomes a connection. Other scalar values become metadata.

Nested metadata objects do not render. Strings, numbers, dates, booleans, and arrays do.

IDs and slugging

IDs, titles used as fallback IDs, and both ends of every link pass through the same slug function:

trim
lowercase
remove characters outside A-Z, a-z, 0-9, underscore, space, and hyphen
turn spaces and underscores into hyphens
trim leading and trailing hyphens
InputResult
my_nodemy-node
Acme Corp.acme-corp
Foo, em dash, Barfoobar
Ünïcode Namencode-name

Accented and non-Latin letters are removed, not transliterated. Give those titles an explicit ASCII id.

Duplicate IDs do not merge. Both cards draw, but only the later card is reachable by link. Treat a duplicate ID as an error.

Weight

Weight changes how strongly a card draws. It does not change the graph.

ValueWeight
lead, critical, primary, key5
major, high4
normal, medium, default3
minor, low2
faint, background, trivial1

Numbers and numeric strings are rounded and clamped to 1 through 5. An unrecognised word silently becomes 3.

Map configuration

Use exactly one block with type: map:

---
type: map
title: Partnerships
layout: LR
types:
  organization: { label: Organization, color: "#4A9BFF" }
  person: "#3FC8D4"
inverse:
  works_at: employs
symmetric: [knows]
---
FieldEffect
titleMap name.
layoutExactly LR for left to right. Every other value gives top to bottom.
typesLabel and color for each node type. A color string is accepted as shorthand.
inversePairs two labels that describe the same directed relationship from opposite ends.
symmetricLabels with no direction.

Those are the only configuration fields read. Plausible keys such as direction, colors, palette, and rankdir do nothing.

Unlisted types receive palette colors in encounter order. A map needs no custom colors to remain readable.

Two map blocks do not merge. The last one replaces the earlier configuration.

Four ways to connect nodes

A rel map

rel:
  employs: [dana, marcus]
  runs: records-portal

A list of single-key maps also works:

rel:
  - employs: dana
  - runs: records-portal

A list of bare targets uses the label links:

rel: [dana, marcus]

Any non-reserved frontmatter key whose value is a wikilink becomes a named connection:

works_at: "[[acme]]"
attendees: ["[[dana]]", "[[marcus]]"]

An inline field in the note

owns:: [[records-portal]]

The label must start with a letter. Letters, numbers, underscores, and hyphens may follow.

Talked to [[dana]] about the rollout.

This creates an untyped mentions connection. Use it only when “mentioned” describes the relationship well enough. [[target|alias]] links to target; the alias changes only displayed text.

A target with no matching ID becomes a gray dashed unresolved card. A self-link is dropped.

Direction normalization

Connections are normalised before they draw.

Passive labels flip

A label ending in by, _by, or -by loses that suffix and swaps its ends. For example, portal owned_by dana becomes dana owned portal.

Declared inverse labels flip

If inverse[label] exists, md2hd replaces the label with its paired value and swaps the ends.

inverse:
  works_at: employs

dana works_at acme becomes acme employs dana.

Normalization runs up to three rounds. Rules can chain. Do not make an inverse value end in _by, because the inverse rule and passive rule will both flip it.

Equivalent connections collapse

Connection identity uses source, target, and a simple stem of the label. The stem lowercases, turns spaces, underscores, and hyphens into spaces, then strips a trailing ed, s, or d.

Between the same two nodes, owns, owned, and own collapse to one connection. The first spelling remains. Irregular verbs such as knew and knows do not collapse.

The reverse voice is kept

One connection can read differently from each end. An organization employs a person; that person works at the organization. md2hd keeps both labels when they were written from opposite ends or declared under inverse.

Inverse labels are global. If assigned_to is paired with owns_action, every owns_action connection in the map receives assigned to as its reverse voice. Use different labels when the same word carries different meanings.

Symmetric connections

Labels under symmetric have no direction. Mirrored connections with the same stem also fold into one symmetric connection. They draw without an arrowhead and with a chevron at each end.

Bare mentions defer

A mentions connection is dropped when a named relationship already connects the same two nodes in either direction.

What appears on the canvas

Open a card to focus it. Incoming connections line up on the left and outgoing connections on the right. Open the background to return to the full map.

Hover highlights cards within two connections. Search filters the map. Type filters hide a category. Dragging saves card positions. The connection-depth controls extend the focused view farther in either direction.

Switch the card panel between Note and Markdown. Editing markdown reparses the map as you type.

Silent failures

Every case below parses without a hard error.

SymptomCause
A card is missingThe block did not open, often because key:value has no space.
Left-to-right layout is ignoredlayout is not exactly LR.
Type colors are ignoredThe map block uses an unread key such as colors.
Some configuration disappearedA later type: map block replaced the first.
An arrow points backwardsA passive suffix or inverse pair flipped it.
A reverse label reads wrong elsewhereAn inverse label was reused with another meaning.
Two connections became oneTheir directions and stemmed labels match.
A gray dashed card appearsA link target has no matching ID.
Two cards share a nameIDs are duplicated; they do not merge.
A card uses a subheading as its titleNo title was set, so the first heading anywhere in the note won.
Several cards say UntitledThey have no title or heading. Their fallback IDs may collide.
Gray unlabeled lines fill the mapBare wikilinks created mentions connections.
Every card has normal weightAn unrecognized weight word fell back to 3.
A written relationship is missingIt pointed back to the same node.

Verify a map

Inside the md2hd repository, run the real parser and read the connection list after normalization:

npx vite-node .agents/skills/writing-md2hd-maps/check-map.ts -- path/to/map.md

For a folder that represents one map, pass the folder. Do not check unrelated maps together; their map blocks would replace each other.

Before shipping a map, confirm:

  1. Every intended block opened.
  2. Exactly one map block exists.
  3. The map block uses only title, layout, types, inverse, and symmetric.
  4. Every ID is unique.
  5. Every link target matches an ID after slugging.
  6. Every weight is a recognized word or a number from 1 through 5.
  7. Every inverse and symmetric label keeps the same meaning everywhere.
  8. Every bare wikilink is meant to draw an unlabeled mentions connection.