The complete reference for writing Orbit maps. A map is a tree: one root, and nodes with children. Write it in orbit-md, Orbit’s markdown (the default, for maps people write), or in YAML (for maps programs write). npx @restless-dreams/orbit example prints a map that uses every feature, and npx @restless-dreams/orbit validate map.md checks one.

Markdown

---
title: Product roadmap
theme: night
---
# 🚀 Product roadmap
What ships this year.

## Research <!-- orbit status: done -->
Six weeks of interviews before we commit to scope.
- [x] User interviews
- Competitor teardown #market

## Build
### Frontend
- [/] App shell
- Offline sync <!-- orbit status: blocked -->
  Waits on the [backend](#backend) sync API.
### Backend

## [Launch](https://example.com/launch-plan)
![The launch poster](images/poster.png)

The tree

Every heading and every list item is a node. Everything else belongs to the node above it.

  • Headings (# to ######) nest by level: a heading’s children are the headings below it, up to the next heading at its own level or higher. A line underlined with === is a # heading, and with --- a ## heading. Headings may be indented up to three spaces and end with closing #s.
  • Lists are children of the heading above them, every list, in the order written next to that heading’s subheadings. Bullets (-, *, +) and numbered items (1., 1)) work the same. Indent an item further than the one above it to nest it under that item; lists go as deep as you like, past the six heading levels. A tab counts as four spaces.
  • The root is the single top heading: the only heading at the highest level used, usually #. Several top headings share a root, as do the top items of a document that is just a list: it’s named after the frontmatter title, a one-line paragraph above them, or “Untitled map”. A list with a single top item is rooted at that item. Text before the root heading belongs to the root.
  • A ```orbit or ~~~orbit fence around the whole map is fine: parseOrbit unwraps it.

Node titles

A heading’s text, or a list item’s first line, is the node’s title, read in this order:

Written Becomes
[ ] Draft / [x] Ship / [/] Build status todo / done / active
🚀 Launch (emoji first) icon 🚀, text Launch
Offline sync #frontend #sprint-3 tags frontend and sprint-3, text Offline sync
Launch [plan](https://…) text Launch plan, link https://…
**Frontend**, *Docs*, `api`, ~~Old idea~~ the text without the symbols
Launch <!-- orbit status: active --> an attribute, inline (see Attributes)
  • Hashtags count at the end of a title, after a space, and need a letter: C# rewrite and Fix #42 stay as written.
  • Links: a title that is all link works the same; with several links, the first is the node’s link.
  • A backslash keeps a character as written: \*not bold\*, ship \#notatag, \[x] not a task.

They combine: ## 📈 [Growth](https://example.com/growth) #q3 <!-- orbit status: active -->.

Notes

A note is longer text shown in the detail sheet, as markdown. Everything under a heading or list item that isn’t a heading or list item is its note: paragraphs, > quotes, code blocks and tables.

  • Under a list item, indent the note under it, as you would write anything under a heading. A line right after the item, with no blank line between, continues the item too.
  • To keep a list inside a note, instead of making it children, quote it with >: headings and bullets inside a quote stay note text.
# Launch
## Research
Questions to answer:
> - Who buys it?
> - Why now?

- Frontend
  Vite and React. Ships behind the beta flag.
- Backend

A link to #id points at another node of the map, the way #heading links point at headings in a page: a note link like [Research](#research), or a node whose link is #research. It flies to that node instead of opening a page.

# Launch
## Build
Starts when [Research](#research) is done.
## [Read the research](#research)
## Research

The id is the node’s text in lowercase with dashes (see Node ids); #Research and GitHub-style #-research find it too. A link to a node the map doesn’t have is reported.

Media

A node shows its image (or its video’s thumbnail) inside it. Its image and video open over the whole window, in the media viewer (Pictures and videos), and its embed in the detail sheet. Put each on a line of its own under the heading, or indented under the list item:

Line Becomes
![Alt text](images/photo.jpg) the image
[Trailer](https://www.youtube.com/watch?v=…) the video: YouTube, Vimeo, or a .mp4, .webm or .mov file
[![Alt text](thumbnail.jpg)](https://vimeo.com/…) the video, with that thumbnail
[Any other link](https://…) note text

A node has at most one image, one video and one embed; extra media lines stay in the note. Embeds (a page shown in a frame) are set with the embed attribute. To read a media line differently, follow it with <!-- orbit contentType: image --> (or video, embed, or none to keep it as note text), and caption it the same way, with <!-- orbit caption: The summit at dawn --> right under it. Relative URLs resolve against baseUrl.

# Trip
- Summit
  ![The summit at dawn](images/summit.jpg)
- Trail
  [Walkthrough](https://www.youtube.com/watch?v=aqz-KE-bpKQ)

Attributes set media too: <!-- orbit image: images/photo.jpg -->.

Attributes

Attributes go in an HTML comment that starts with orbit, written as YAML:

## Build
<!-- orbit
color: "#F5A3A8"
status: active
priority: 1
tags: [frontend, sprint-3]
-->

- Frontend <!-- orbit status: active -->
- Backend
  <!-- orbit
  image: diagrams/backend.png
  note: Talks to the billing service.
  -->
  • Block form: on the lines after a heading, or indented under a list item. Any number of keys.
  • Inline form: at the end of a heading or list item’s line. Separate several with commas: <!-- orbit status: active, priority: 1 -->.
Key Value Example Effect
color any CSS color "#F5A3A8", tomato The node’s color, instead of its branch’s. Its children take shades of it.
icon emoji or short symbol 🚀, "★" Shown before the text. Same as an emoji at the start of the title.
status text active, done, blocked Shown in the detail sheet; some words get a colored dot (see Statuses).
priority number 1 Shown in the detail sheet.
tags list of text [frontend, sprint-3] Shown in the detail sheet. A single word works too. Hashtags at the end of the title add to them.
collapsed true / false true Hides the node’s children (they show as a “+N” count) until you fly into it.
link URL, or #id https://example.com Offered as a button when the node is in focus. Same as a link in the title. #id goes to another node (see Links between nodes).
note text Ships in May. The node’s note. Same as a paragraph or blockquote.
noteFormat markdown / plain plain How the note is shown. Default markdown.
image URL, or { src, alt, caption } images/photo.jpg The node’s image.
video URL, or { src, provider, thumbnail, caption } https://youtu.be/… The node’s video. provider: youtube, vimeo or native.
embed URL, or { src, caption } https://example.com/demo A page shown in a frame in the detail sheet.
caption text The summit at dawn. Caption for the media line just above it, or for the one image, video or embed set next to it. Anywhere else, for the node’s video, embed or image (in that order).
id text launch The node’s id, instead of one made from its text (see Node ids).
type text milestone The node’s type. A type your code defines sets its look, content and actions (see Extending). Any case.
contentType image / video / embed / none embed Markdown only: how to read the media line just above.

Any other key is kept as a custom attribute in node.attrs, for your own code (<!-- orbit owner: Ana -->). A key that looks like a misspelled attribute (colour, stauts) is kept too, with a warning that suggests the right one.

Statuses

status is free text, shown as written. These words (any case) also get a colored dot on the node and in the detail sheet:

Dot Words
active (blue) active, doing, in progress, wip, started, ongoing
done (green) done, complete, completed, shipped, finished, closed
blocked (red) blocked, stuck, at risk, failed
todo (gray) todo, to do, planned, backlog, next, open

Other words get a neutral dot. Checkboxes set todo ([ ]), done ([x]) and active ([/]).

Document options

Set in the frontmatter (markdown) or as top-level keys (YAML). Options passed in code win over these.

Key Value Default
title text the root’s text The map’s accessible name and SVG title. Names the root when a document has no single top heading.
theme paper, night, glass, glass-night, cosmos, blueprint, ink, contrast, flat, flat-night, material, material-night, liquid, liquid-night or auto paper See Theming.
view focus or all focus The first view: one node with its children around it, or the overview of the whole tree.
palette list of colors the theme’s The root’s color, then one per top-level branch, cycling. A comma-separated string works too.
maxDepth number all levels Levels below the root to draw: 1 is the root and its children.
showRings true / false true Draw the orbit rings.
interactive true / false true false draws a still picture.
animation true / false false Let the orbit drift slowly on its own.
speed seconds 90 Seconds per turn of that drift.
height pixels 420 Height of <orbit-map> elements and marked placeholders.
baseUrl URL Resolves relative image, video and link URLs.
className text Class names added to the map’s element.
schemaVersion number The version of the format the document is written for: 1. See Versions.
flavor markdown or yaml Forces the flavor (see Which flavor a document is).

Flags also read yes, no, on and off.

YAML

For maps that programs generate:

title: Product roadmap
theme: night
nodes:
  - text: Product roadmap
    icon: 🚀
    note: What ships this year.
    children:
      - text: Research
        status: done
        children: [User interviews, Competitor teardown]
      - text: Build
        status: active
        children:
          - text: Frontend
            children:
              - App shell
              - text: Offline sync
                status: blocked
      - text: Launch
        link: https://example.com/launch-plan
        image: images/poster.png
  • nodes is a list with the root node. Several top-level nodes share a root named after title, as in markdown.
  • A node is a map with text and optionally children, plus any attribute (icon, color, status, link, note, image, …) as a key. Custom keys are kept in node.attrs.
  • A plain string in children is a node with just that text.
  • Document options go at the top level, next to nodes.

Which flavor a document is

parseOrbit reads a document as YAML when its frontmatter says flavor: yaml, its first line is nodes:, or the whole text reads as a YAML map: one with a nodes: list, or one that starts with a document option (title:, theme:, view:, …). Anything else is markdown, so a first line like Status: draft, or a note that starts with nodes:, stays markdown. parseOrbitMarkdown and parseOrbitYAML skip the detection.

What saving keeps

Orbit never drops what a document says, even what it doesn’t read:

  • Saving keeps the file as it was written. serializeOrbitMarkdown (and a map’s toMarkdown()) write a document parsed from markdown back line for line, and only the ideas an edit changed are written again: a renamed idea gets a new title line, a checked-off item a new checkbox, a moved idea takes its lines along. So a note written by hand or by another app keeps its formatting, its frontmatter keys, its HTML comments and rules, and its link definitions at the end. { keepSource: false } writes the whole map in Orbit’s own form instead; so does orbit convert.
  • Keys and values Orbit doesn’t read are kept. Frontmatter and top-level YAML keys it doesn’t know are on document.extra, and both serializers write them back, with any changes an app makes to them: an app can keep a setting of its own there. A document without extra changes none of them, so no key another app wrote is lost; an extra object, even {}, changes or removes them. An attribute Orbit doesn’t know is a custom attribute on node.attrs; a value it can’t read for one of its own (a list for status, a word for priority) is reported and kept there under its own name. A picture or video keeps fields Orbit doesn’t use (width, a provider it doesn’t know).
  • A file that isn’t a map yet (only paragraphs) is a root with the text as its note: adding the first idea gives it a heading and keeps the text.

Versions

A document says which version of the format it’s written for with schemaVersion; none means 1, the current one.

  • Older documents are upgraded as they’re read. When the format changes, Orbit brings documents written for the version before up to date as it reads them (a format-upgraded note says so), and the next save writes them as the new version, changing only what the upgrade changed.
  • Newer documents are read as well as they can be, and reported with unsupported-schema-version, an error: apps should open them without letting them be edited. Nothing in them is lost by saving, but what this version doesn’t know of them isn’t understood either.
  • Node types have versions of their own, for the fields their nodes keep in attributes: type: task@2 says a node was written for version 2 of task, and none means 1. A type that changes its fields says so with version and brings older nodes up to date with upgrade (see Extending Orbit); a node written for a newer version of its type is left as it is.

Node ids

Each node gets an id from its text: lowercase, accents folded, other letters kept (Investigación → investigacion, 研究 → 研究, Q3 plan → q3-plan). Repeated ids get -2, -3, … in tree order. Set id to choose one; ids are what renderer.flyTo(id), the focus export option and the React focusedId prop take.

Checking a map

Parsing is forgiving: when something is wrong but the map can still be drawn, the parser works around it and records a diagnostic with a code, the line, a severity and a message that says how to fix it. { strict: true } throws an OrbitParseError on the first one instead.

npx @restless-dreams/orbit validate map.md          # every problem as map.md:line: severity: message [code]
npx @restless-dreams/orbit validate --json map.md   # the same as JSON: valid, flavor, root, nodes, depth, diagnostics

validate exits with 1 when the map has problems (add --loose to accept the ones the map works around).

Code Meaning
empty-document Nothing to draw.
no-structure No headings or lists (markdown), or no nodes: list (YAML).
synthetic-root Several top headings, items or YAML nodes and no title, so the root is called “Untitled map”.
invalid-frontmatter The frontmatter isn’t valid YAML key: value pairs.
unknown-frontmatter-key A frontmatter key Orbit doesn’t know: kept and written back, but not used. The message names the one you probably meant.
unknown-yaml-key The same, for a YAML document’s top-level keys.
invalid-option-value A document option with a value Orbit can’t use, such as an unknown theme or a flavor other than markdown or yaml.
unsupported-schema-version The document is written for a newer version of the format (an error: open it without editing it).
format-upgraded The document was written for an older version of the format and read as the current one; saving writes it in the current one.
invalid-attributes An attribute block isn’t valid YAML, or a value has the wrong type: kept as written, but not used.
unknown-attribute A key that looks like a misspelled attribute, kept as a custom one.
media-missing-source An image, video or embed without a URL.
unknown-content-type contentType isn’t image, video, embed or none.
caption-without-media A caption on a node with no image, video or embed.
unknown-link-target A #id link points at no node of the map; the message suggests the id you probably meant.
invalid-yaml The YAML document can’t be read.
ignored-node-entry A YAML node that is neither a map nor text.