---
title: "Obsidian: import and export a vault"
description: "The add-on @skaile/forge-editor-obsidian: importVault plans one Document per note with frontmatter as properties, wikilinks as reference chips and ^id block ids kept; exportVault writes them back; a command uploads a vault to an editor server."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/obsidian"
---
# Obsidian: import and export a vault

> The add-on @skaile/forge-editor-obsidian: importVault plans one Document per note with frontmatter as properties, wikilinks as reference chips and ^id block ids kept; exportVault writes them back; a command uploads a vault to an editor server.

The add-on `@skaile/forge-editor-obsidian` moves notes between an [Obsidian](https://obsidian.md) vault and the editor. The database stays canonical ([ADR-0001](https://github.com/skaile-ai/forge-editor/blob/main/docs/adr/0001-database-canonical-markdown-export.md)): a vault is a file format for import and export, the editor does not watch a folder and does not keep a vault in sync.

The add-on has no editor feature and no UI. It is a set of pure functions, a small upload helper and a command. The host needs **no wiring**: install the package where the script runs.

## Import

```ts
import { importVault } from '@skaile/forge-editor-obsidian'

const plan = importVault(files) // files: { path: 'Notes/Note B.md', text: '…' }[]
plan.documents // one PlannedDocument per note
plan.unresolved // links no note answers
plan.warnings // frontmatter or Templater code outside the supported subset, a missing heading or block id
plan.skipped // paths that are no notes (attachments, canvases, folders that start with a dot)
```

`importVault` writes nothing. Each `PlannedDocument` is `{ id, path, title, folder, template, properties, content }`. `content` is the Document's TipTap JSON: one Typed block of the Block type `document` that holds the note's blocks, so `[[Note]]` has a block to point at. Its `meta` is the note's frontmatter, its tags are the note's tags, its title is the file name.

<table>
<thead>
  <tr>
    <th>
      In the note
    </th>
    
    <th>
      In the Document
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Frontmatter keys (the subset <code>
        parseFrontmatter
      </code>
      
       reads: scalars, flow lists, block lists)
    </td>
    
    <td>
      <code>
        properties
      </code>
      
      , the <code>
        meta
      </code>
      
       of the wrapping block. Nested maps are reported in <code>
        warnings
      </code>
      
       and skipped
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tags
      </code>
      
      , <code>
        tag
      </code>
      
       in frontmatter
    </td>
    
    <td>
      The tags of the wrapping block, in the editor's tag name form (<code>
        Ideas/New
      </code>
      
       → <code>
        ideas/new
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        aliases
      </code>
      
       in frontmatter
    </td>
    
    <td>
      Kept as a property, and used to resolve links
    </td>
  </tr>
  
  <tr>
    <td>
      Body
    </td>
    
    <td>
      The library's Markdown parser (the same one the server uses for <code>
        documentOps
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        text ^id
      </code>
      
       at the end of a paragraph or heading
    </td>
    
    <td>
      The block's <code>
        id
      </code>
      
       is that id; the marker leaves the text
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ^id
      </code>
      
       on a line of its own after a list, quote, table or code block
    </td>
    
    <td>
      The <code>
        id
      </code>
      
       of the block before it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        [[Note]]
      </code>
    </td>
    
    <td>
      A <code>
        reference
      </code>
      
       chip to the note's wrapping block
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        [[Note#Heading]]
      </code>
      
      , <code>
        [[Note#^id]]
      </code>
    </td>
    
    <td>
      A chip to that heading or block; when the note has none, the chip points at the note and <code>
        warnings
      </code>
      
       says so
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        [[Note|alias]]
      </code>
    </td>
    
    <td>
      The chip with the alias as its label
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        [[#Heading]]
      </code>
    </td>
    
    <td>
      A chip to a heading of the same note
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ![[Note]]
      </code>
      
      , <code>
        ![[Note#Heading]]
      </code>
      
       alone in a paragraph
    </td>
    
    <td>
      A <code>
        blockEmbed
      </code>
      
       block (the whole note: its children; a heading or block: that block)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        [[Missing]]
      </code>
    </td>
    
    <td>
      Stays text, and is listed in <code>
        unresolved
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now() %>
      </code>
    </td>
    
    <td>
      <code>
        {{date}}
      </code>
      
      , see below
    </td>
  </tr>
</tbody>
</table>

A link names its note by file name, by path (`[[folder/Note]]`) or by an `aliases` entry, without regard to case. Where several notes match, the one in the same folder wins, then the one with the shortest path. Links are resolved in a second pass, after every note has been read, so a link may point at a note that comes later in the vault.

### Block ids

The ids of blocks carry the links: a chip is `{ target: blockId, documentId }`. `importVault` keeps every `^id` as the block's `id` and gives all other blocks a new one. The first of a repeated id wins and the rest are reported in `warnings`. The ids are valid inside one Document only; a link `[[Note#^id]]` is resolved against the ids of that note.

### Templater

Templater is code, and none of it runs. A small subset has a Dokumentvorlage placeholder (see [Templates](https://skaile-ai.github.io/forge-editor/guides/templates)) and is translated:

<table>
<thead>
  <tr>
    <th>
      Templater
    </th>
    
    <th>
      Placeholder
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        <% tp.date.now() %>
      </code>
    </td>
    
    <td>
      <code>
        {{date}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now("YYYY-MM-DD") %>
      </code>
    </td>
    
    <td>
      <code>
        {{date:iso}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now("HH:mm") %>
      </code>
      
       (a clock format)
    </td>
    
    <td>
      <code>
        {{time}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now("DD.MM.YYYY") %>
      </code>
      
      , <code>
        "L"
      </code>
    </td>
    
    <td>
      <code>
        {{date:short}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now("LL") %>
      </code>
      
      , <code>
        "D MMM YYYY"
      </code>
    </td>
    
    <td>
      <code>
        {{date:medium}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.date.now("LLL") %>
      </code>
      
      , a format with <code>
        MMMM
      </code>
    </td>
    
    <td>
      <code>
        {{date:long}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.file.title %>
      </code>
    </td>
    
    <td>
      <code>
        {{title}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        <% tp.file.cursor() %>
      </code>
    </td>
    
    <td>
      <code>
        {{cursor}}
      </code>
    </td>
  </tr>
</tbody>
</table>

Every other `<% … %>` stays as written and is named in `warnings`. The notes of the vault's templates folder (`templatesFolder`, default `Templates`, `null` for none) are planned into the folder `Dokumentvorlagen` and have `template: true`. A Document made from one of them through `documentCreate` with `templateId` gets the placeholders expanded.

### Options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        markdown
      </code>
    </td>
    
    <td>
      The Markdown parser, <code>
        createMarkdownBlocks({ blockTypes })
      </code>
      
       of the host, when notes hold <code>
        :::
      </code>
      
       fences of the host's own Block types
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        templatesFolder
      </code>
    </td>
    
    <td>
      The vault's templates folder, default <code>
        Templates
      </code>
      
      , <code>
        null
      </code>
      
       for none
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        newId
      </code>
    </td>
    
    <td>
      Where Document ids and missing block ids come from, default <code>
        crypto.randomUUID
      </code>
    </td>
  </tr>
</tbody>
</table>

## Create the Documents

Links between Documents name provisional ids. Create the Documents first, then swap the ids and store the content:

```ts
import { importVault, uploadVault } from '@skaile/forge-editor-obsidian'

const plan = importVault(files)
await uploadVault(plan.documents, { url: 'https://editor.example.com', token })
```

`uploadVault` calls `documentCreate` for every Document, then `documentPut` with the content after `bindDocumentIds` (`API` and `routeUrl` from `@skaile/forge-editor/core` name the routes). `token` goes out as `Authorization: Bearer <token>` and `cookie` as `Cookie: <cookie>`; what the host's `authenticate` makes of them is the host's choice. The Reference app reads its session cookie only, so a bearer token gets `401` there; use `cookie`. A Document goes into its folder when the server has it. The routes cannot create folders, so a note whose folder is missing lands in the root and `warn` says so. The routes do not know a note by its path: a second run creates the Documents again.

## Export

```ts
import { exportVault } from '@skaile/forge-editor-obsidian'

const files = exportVault(documents) // documents: { id, title, folder?, content }[] → { path, text }[]
```

One `.md` per Document, at `<folder>/<title>.md`; two Documents with the same path get ` (2)` added. The Markdown is the library's Markdown export (the same as the source mode), with these changes:

- A Document that is one Typed block of the Block type `document` (what `importVault` plans) is unwrapped: its `meta` and tags are the frontmatter, its body the text. A `derivation` or `edges` entry stays in the database.
- A reference chip or embed to a Document in the export becomes `[[Note]]`, `[[Note#Heading]]`, `[[Note#^id]]` or `![[…]]`, with the chip's label as the alias. Where two Documents share a title, the link names the path. A chip to a Document that is not in the export stays `[@…]`.
- A block that a link names gets its id as `^id`: at the end of a paragraph or heading or list item, else on a line of its own after the block. The ids that came from a vault (`intro`, `key-point`) are written for every block that has one. A block the editor numbered (a UUID) that no link names gets none: the database keeps that id (ADR-0001).
- A link that cannot name its block (the block sits inside a quote, or its id is not a legal Obsidian id) points at the note, and `warn` says so.
- Typed blocks of other types are written as `::: kind` fences. Obsidian shows them as text.

A vault that is exported and imported again keeps its notes' frontmatter, links and `^id` lines.

## Command

```sh
bunx @skaile/forge-editor-obsidian import ./MyVault --url https://editor.example.com --token $TOKEN
```

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        --url
      </code>
    </td>
    
    <td>
      The editor server, also <code>
        EDITOR_URL
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --token
      </code>
    </td>
    
    <td>
      Sent as the bearer token (<code>
        Authorization: Bearer
      </code>
      
      ), also <code>
        EDITOR_TOKEN
      </code>
      
      ; for a host whose <code>
        authenticate
      </code>
      
       accepts a bearer token
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --cookie
      </code>
    </td>
    
    <td>
      Sent as the <code>
        Cookie
      </code>
      
       header, also <code>
        EDITOR_COOKIE
      </code>
      
      ; for a host with a session cookie
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --templates
      </code>
    </td>
    
    <td>
      The vault's templates folder (<code>
        ""
      </code>
      
       for none)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --dry-run
      </code>
    </td>
    
    <td>
      Plan and print the counts, call nothing; <code>
        --url
      </code>
      
       is then optional
    </td>
  </tr>
</tbody>
</table>

It prints the warnings and unresolved links, and one line per Document with the id the server gave.

### Against the Reference app

The Reference app of this repository has no bearer tokens; it knows a caller by its session cookie. Sign in as one of its users with `POST /api/auth/switch` (`anna`, `ben` or `bernd`) and hand the `set-cookie` pair to `--cookie`:

```sh
COOKIE=$(curl -si -X POST http://localhost:3056/api/auth/switch -H 'content-type: application/json' -d '{"username":"anna"}' | grep -i '^set-cookie:' | sed 's/^[^:]*: *//; s/;.*//')
bunx @skaile/forge-editor-obsidian import ./MyVault --url http://localhost:3056 --cookie "$COOKIE"
```

`$COOKIE` is `nuxt-session=Fe26.2**…`. The Documents belong to that user.

## Not included

- Attachments (images, PDFs) are skipped. Obsidian canvases, Dataview queries, callouts' foldable state and inline `#tags` outside frontmatter stay as the Markdown parser reads them.
- No two-way sync. The `.obsidian` folder and every folder that starts with a dot are never read.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
