Files
lanework/DESIGN/15-import-export.md
T
rzen b0c134a896 A board leaves as one file and comes back as one — headings are lanes, rows are cards, position is the order
File ▸ Export ▸ writes the frontmost board as Obsidian Kanban Markdown, a
plain Markdown outline, or RFC 4180 CSV; File ▸ Import Board… reads any of
the three back into a fresh board, format detected rather than asked. Every
format encodes order as document position, so an export writes no ranks and
an import mints them in parse order on the ordinary create path. Lossy
exports post a warning-tone loss row naming the comments and attachments the
destination cannot carry. Convert-once: nothing watches, nothing merges.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 08:38:06 -04:00

14 KiB

Import and Export

Status: v1 settled and built (2026-08-09, Pipeline card 332e4377). The format inventory below is the whole of the brainstorm that produced it; the v1 set is shipped, and the deferred and rejected lists are recorded so neither gets re-litigated by accident.

Lanework's on-disk format is already the most open thing about it: a board is folders and Markdown, and anybody's tools can read it. Import and export are therefore not a data-liberation feature — the data is already liberated — but a shape feature. What a foreign format carries is not the bytes of a card but the arrangement of them: which lane, in what order. Everything below follows from that.

The formats (v1)

Three, both directions, all three converting a whole board to or from a single file.

Obsidian Kanban plugin Markdown

One .md in the shape mgmeyers' plugin writes: kanban-plugin: board frontmatter, ## headings as lanes in lane order, - [ ] list items as cards in card order, multi-line bodies as continuation lines indented two spaces under their item.

  • The frontmatter is spelled the way the plugin spells it — blank lines inside the --- delimiters — so a file exported from here is byte-shaped like one the plugin wrote.
  • No # board title is written: the plugin has no such concept (a board's name is its file's name), so an H1 would read in Obsidian as a stray heading with no cards under it.
  • Nothing is exported checked, including a lane called Done. Lanework has no done flag, and matching lane titles against a word list to synthesize one would be the exporter inventing data. The import side ignores the marker for the same reason: - [x] becomes an ordinary card.
  • The plugin's trailing %% kanban:settings %% block and its ## Archive section are not written. Both are the plugin's own state about a board rather than the board; a settings block synthesized from defaults would be this app asserting preferences inside another app's file. On import both are handled: the comment block is skipped rather than read as a card, and an ## Archive heading arrives as an ordinary lane named Archive — the honest landing place for cards in an app with no archive.

Plain Markdown outline

The same document minus the plugin marker: an optional # board title, ## lanes, - items, the same indented continuations. The "paste this board into a doc, a PR, or a chat message" format, and nearly free once the flavour above exists.

CSV

RFC 4180, UTF-8, no BOM, CRLF record terminators, header row lane,title,body,created,modified — one row per card with the lane title repeated down the column. Bodies are quoted and escaped, so a multi-line card survives the trip.

CRLF is the one place the app's own LF-everywhere rule (01-storage-format.md § Encoding and line endings) deliberately does not reach: that rule governs the files a board is made of, and a CSV is an interchange document written for other people's tools, where the standard names one terminator and every reader takes both.

The two stamps are export-only. Nothing reads them back — see An imported board is born today.

Order is position, in every direction

Every one of these formats encodes order as document position: heading order, list-item order, row order. So an export writes nothing down about ranks, and an import mints them in parse order — 1024, 2048, 3072 — which is exactly where the app's own append (Ranks.append(toVisible:)) would have put them had the user typed the cards in that sequence. The symmetry is most of why all three formats are cheap, and it is why the round-trip suite is this feature's primary proof rather than a nicety.

Export is three rows; import is one

File ▸ Export ▸ {Obsidian Kanban Markdown…, Markdown Outline…, CSV…} — the frontmost board, each row opening a save panel already pointed at the right extension. Three named rows rather than one row with a format popup: a popup makes the menu row itself uninformative (nothing about "Export…" says CSV is on offer), and menu titles are API in this app (11-command-nexus.md ▸ Configurable bindings), so a row whose meaning lived in a popup would be one title standing for three commands.

An export runs the close flush first, like Duplicate, Save as Template and Share before it, so a card window's unsaved keystrokes are on disk before the snapshot is serialized. An export is a read: it writes nothing into the board's own tree, so the read-only lock does not close the rows — a board on a read-only volume is exactly the board somebody wants a copy of. The one carve-out kept is the focused-inline-editor rule, since an open rename holds the one pending change no flush can reach.

File ▸ Import Board… — one row, and no format picker: the format is detected. The plugin's frontmatter marker identifies an Obsidian Kanban file outright; a .csv extension answers for the table; a Markdown extension answers for the outline; a file with no useful extension is sniffed (a consistent multi-column table with no Markdown structure anywhere in it is CSV, everything else is outline). The fallback is the lenient outline parser, which cannot fail. Asking users to classify their own file would make them do the one part of this the app can do reliably.

The row sits beside File ▸ Open… rather than beside Export, and is available with no board in front: the two rows next to each other are the two ways a board arrives from disk.

An import always creates a fresh board

Open panel (which file), save panel (where the board goes), then the board opens through the ordinary open path — registering, bookmarking and titling itself like any other. There is no merge-into-the-open-board in v1. An import that landed lanes inside a live board would need every reconciliation rule the paste path already carries, for a gesture nobody has asked for; and a fresh board keeps the whole feature reversible by deleting one folder.

The save panel's name wins over any title parsed out of the document, so display name and folder name start out matching (01-storage-format.md § Board naming) — the parsed title is what seeded the suggestion, not what overrides the answer. That is 09-templates.md ▸ Instantiation's rule, read one flow over.

The tree is built by the ordinary Writer (createBoard/createLane/createCard/writeBody), so an imported board is indistinguishable from a hand-built one: schema: 1, kind keys, lowercase-UUID folder names, a seeded .gitignore, a current agent guide. Atomicity is the template engine's, verbatim — the destination is created by the import and removed by it on every exit that is not a board, and an occupied name is refused rather than clobbered.

An imported board is born today. No importer reads a created or modified cell: the create path stamps both from one fresh Date, exactly as 09-templates.md rules for instantiation ("a new board is born today, not forked"). Backdating an import would claim a provenance the app cannot verify from a spreadsheet cell.

Lossy exports say so

None of the three formats can carry comments or attachments. So an export that leaves either behind posts a warning-tone loss row naming the counts — "Exported without 12 comments and 3 attachments — CSV carries neither" — through the same BannerCenter machinery and in the same voice as the relocation and skipped-folder notices (02-architecture.md § The banner surface).

The class is the right one for the same reason the skipped-folders notice is: the operation succeeded and only the payload the destination cannot hold stayed behind. A signpost would rank last and may collapse behind "+N more", and an export the user believes is complete is exactly the harm; a one-shot would be a lie, since it carries a BoardWriteError and the write succeeded.

A lossless export says nothing at all. The file is where the user pointed, and a row confirming that would be noise.

Two smaller omissions are recorded here rather than counted in a banner, because unlike comments and attachments they are fields most boards leave empty or would not miss: a lane's body (its description or WIP policy — a Markdown lane is its heading line, and CSV's grain is one row per card), and every styling key (background, icon, iconColor, width, collapsed) plus the reserved keys. Two lanes that share a title also merge on a CSV re-import, which is an inherent property of a flat table rather than a choice.

Convert once; there is no sync

Every one of these conversions is one-shot. Exporting does not create a link, importing does not create a link, and nothing watches an exported file for changes. Editing a board in Obsidian's plugin and reopening it here does not merge — it imports again, as a second board.

This is ruled out explicitly rather than left unsaid, because a real round trip is a materially bigger feature than an importer and an exporter: it needs a file-watching story, a conflict story, and an identity story for objects that have no stable id in any of these formats (position is the only key a Markdown outline has, and position is exactly what editing changes). Whatever durable two-way story Lanework eventually wants belongs to the sync workstream, not here.

Obsidian vault interop is free, and should be said out loud

A Lanework board folder is already very nearly a valid Obsidian vault, and this costs no code at all:

  • Every card is one index.md with YAML frontmatter — Obsidian's own file shape.
  • Attachment links are relative paths into the card's attachments/ folder (01-storage-format.md § Attachments), which render in Obsidian's preview exactly as they render here.
  • Unknown frontmatter keys ride along untouched in both directions, so Obsidian's properties and this app's schema coexist without either rewriting the other's.
  • Nothing in the board tree is a database, a cache, or a binary sidecar that a second editor could corrupt by writing normally.

What Obsidian does not see is the board shape: it reads a flat collection of notes, because lanes and order live in folder names and an order key rather than in anything Obsidian models. That is the honest half of the claim, and the Obsidian Kanban plugin export above is what closes it for anyone who wants the board shape inside their vault.

This is a positioning note as much as a technical one. "Files-first" is easy to say; "point Obsidian at your board folder and it works" is the demonstration.

Deferred — real, but not v1

  • Trello JSON import — the highest-demand row by a distance (every switcher has a Trello board), and the reason it is not in v1 is that it is not a shape transform: attachments are cloud URLs that need re-downloading and can expire, and comments have to be pulled out of a capped activity log with an authorship question attached. Worth doing after the cheap wins prove the import surface once.
  • Canonical JSON dump — the only format that could be fully lossless (comments, colours, modified-by, timestamps). Cheap once the serialization is being touched anyway, and the natural wire shape for scripting. Sequenced late deliberately: it competes head-on with "just zip the .kanban package", which already wins on fidelity for zero app code and which File ▸ Share… already does.
  • TaskPaper and OPML — both are the outline exporter with different delimiters, so both are nearly free whenever that code is next open; the audience is small and aging. Bundle them together rather than scheduling either alone.
  • Notion import — a genuine demand pool, with a real risk of overselling: Notion's value is databases and relations that do not map onto lanes and cards, and the importer needs a user-driven mapping step (which property is the board view) that none of the v1 formats need. Wait for somebody to ask.
  • OmniFocus (via OPML) and Reminders (via EventKit) — plausible and individually cheap, no demand signal. Reminders in particular is a live OS API rather than a file format and probably belongs with a future quick-capture feature rather than here.

Rejected

  • Tracker sync, GitHub Projects included — 00-vision.md's explicit non-goal for this scope ("No tracker integrations… the schema reserves remote/remote-state… nothing here is designed for them"). GitHub Projects has no export file at all: it is a GraphQL API behind OAuth, which makes it a live-sync feature, not an import format. It belongs to whatever Teams pass eventually happens.
  • Things (official export) — there is no first-party export surface to target. The only routes are reading its private SQLite database or asking the user to run a community script first, and neither is a target worth building against. If it is ever asked for, point people at the community TaskPaper exporters and let a deferred TaskPaper importer pick it up secondhand.
  • Print/PDF as an interchange format — already shipped as presentation (File ▸ Print…, board or card, configurable components, named profiles). Noted here only so it is not re-proposed as an export row.

Open questions

  • The command Nexus owes rows for all four items — File ▸ Export ▸ (three) and File ▸ Import Board… are not yet in 11-command-nexus.md's inventory. Neither takes a default chord.
  • The v1 rulings on this page were made by the main session on the owner's behalf (the owner's own word was "proceed as per your recommendation for v1"): the export and import surfaces, the fresh-board-only import, the loss-row posture, the convert-once ruling, and the decision to treat vault interop as documentation. Each is flagged for review on the Pipeline card's comment thread.