> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Obsidian Vault Ingestion

> Sync a local Obsidian vault into Cognee memory so edited notes replace their old versions and deleted notes are forgotten

A minimal guide to turning an Obsidian vault into Cognee memory. `obsidian_source()` returns a dlt resource you pass straight to `remember()`, and each `*.md` note becomes one document whose id is its path in the vault. Unlike `remember(<folder>)`, which stores a note once, re-running the sync replaces edited notes and forgets deleted or renamed ones.

## Before You Start

* Complete [Quickstart](https://docs.cognee.ai/getting-started/quickstart.md) to understand basic operations
* Ensure you have [LLM Providers](https://docs.cognee.ai/setup-configuration/llm-providers.md) configured — the script needs `LLM_API_KEY` to extract the graph and answer the question
* Read [Datasets](https://docs.cognee.ai/core-concepts/further-concepts/datasets.md) — the vault is synced into its own `obsidian_vault` dataset
* No extra install, account, or token is needed: the connector ships with Cognee and only reads files from a local folder
* Run the script against a throwaway Cognee instance (a scratch `SYSTEM_ROOT_DIRECTORY` and `DATA_ROOT_DIRECTORY`) — it starts by wiping the whole instance, not just this dataset

> [!WARNING]
>
> `prune_data()` and `prune_system(metadata=True)` delete every dataset in the instance the script runs against. Remove those two calls before pointing the script at memory you want to keep.

## Code in Action

```python theme={null}
import asyncio
import tempfile
from pathlib import Path

import cognee
from cognee.tasks.ingestion.connectors import obsidian_source

# Keep the vault in its own dataset so it is easy to inspect and forget.
DATASET_NAME = "obsidian_vault"

# Routing kwargs for remember(). Pass the same ones on every later sync.
#   write_disposition="merge" is REQUIRED: the add pipeline defaults to "replace",
#     which would wipe the whole synced vault on the next sync.
#   max_rows_per_table=0 guarantees no per-table read cap applies (even if
#     DLT_MAX_ROWS_PER_TABLE is set), so forget-on-delete compares against the
#     *entire* synced vault.
#   incremental_loading is left at its default (True) so a re-sync only builds
#     the graph for new or changed notes instead of the whole vault again.
OBSIDIAN_REMEMBER_KWARGS = {
    "primary_key": "id",
    "write_disposition": "merge",
    "max_rows_per_table": 0,
    "self_improvement": False,
}

NOTES = {
    "People/Ada Lovelace.md": "Ada Lovelace is the lead engineer of Project Atlas.\n",
    "Projects/Atlas.md": (
        "---\n"
        "title: Project Atlas\n"
        "tags: [engineering]\n"
        "---\n"
        "Project Atlas builds the routing engine. Ada Lovelace leads it.\n"
    ),
    "Reading list.md": "Grace Hopper wrote the compiler paper that Project Atlas cites.\n",
}

QUESTION = "Who leads Project Atlas?"


async def sync_and_ask(vault: Path) -> None:
    source = obsidian_source(vault_path=vault)
    await cognee.remember(source, dataset_name=DATASET_NAME, **OBSIDIAN_REMEMBER_KWARGS)
    print("Sync stats:", source.cognee_sync_stats)

    answer = await cognee.recall(
        query_text=QUESTION,
        query_type=cognee.SearchType.GRAPH_COMPLETION,
        datasets=[DATASET_NAME],
    )
    print(answer[0].text)


async def main():
    # Start from a clean slate so the demo is reproducible. WARNING: this wipes the whole
    # cognee instance, every dataset, so use a throwaway instance.
    await cognee.prune.prune_data()
    await cognee.prune.prune_system(metadata=True)

    with tempfile.TemporaryDirectory() as folder:
        vault = Path(folder)
        for name, text in NOTES.items():
            (vault / name).parent.mkdir(parents=True, exist_ok=True)
            (vault / name).write_text(text)

        # ── First sync: every note is read and extracted ──────────────────
        print("\n=== First sync ===")
        await sync_and_ask(vault)

        # ── Change the vault: edit one note, delete another ───────────────
        (vault / "Projects/Atlas.md").write_text(
            "---\n"
            "title: Project Atlas\n"
            "tags: [engineering]\n"
            "---\n"
            "Project Atlas builds the routing engine. Grace Hopper leads it now.\n"
        )
        (vault / "Reading list.md").unlink()
        (vault / "People/Ada Lovelace.md").write_text("Ada Lovelace left the company.\n")

        # ── Second sync: only the changed notes are extracted again ───────
        # The edited notes replace their old documents and the deleted note
        # is forgotten, so the old answer does not come back.
        print("\n=== Second sync ===")
        await sync_and_ask(vault)


if __name__ == "__main__":
    asyncio.run(main())
```

## What Just Happened

### Step 1: Configure the Sync

```python theme={null}
OBSIDIAN_REMEMBER_KWARGS = {
    "primary_key": "id",
    "write_disposition": "merge",
    "max_rows_per_table": 0,
    "self_improvement": False,
}
```

These are the routing options for `remember()`, reused on every sync. `write_disposition="merge"` upserts notes by their `id` (their path in the vault); with the default `"replace"`, the next sync would wipe the whole synced vault. `max_rows_per_table=0` disables any per-table read cap, so forget-on-delete compares against the entire vault.

### Step 2: Sync the Vault and Ask

```python theme={null}
async def sync_and_ask(vault: Path) -> None:
    source = obsidian_source(vault_path=vault)
    await cognee.remember(source, dataset_name=DATASET_NAME, **OBSIDIAN_REMEMBER_KWARGS)
    print("Sync stats:", source.cognee_sync_stats)

    answer = await cognee.recall(
        query_text=QUESTION,
        query_type=cognee.SearchType.GRAPH_COMPLETION,
        datasets=[DATASET_NAME],
    )
    print(answer[0].text)
```

`obsidian_source(vault_path=...)` builds a dlt resource over every `*.md` note in the vault. `remember()` loads the notes into the `obsidian_vault` dataset and builds the graph, and `source.cognee_sync_stats` reports counts such as `synced`, `deleted`, and `failed`. [`recall()`](https://docs.cognee.ai/core-concepts/main-operations/recall) then asks the same question against that dataset.

### Step 3: Change the Vault

```python theme={null}
        (vault / "Projects/Atlas.md").write_text(
            "---\n"
            "title: Project Atlas\n"
            "tags: [engineering]\n"
            "---\n"
            "Project Atlas builds the routing engine. Grace Hopper leads it now.\n"
        )
        (vault / "Reading list.md").unlink()
        (vault / "People/Ada Lovelace.md").write_text("Ada Lovelace left the company.\n")
```

The script edits two notes and deletes a third, the way you would in Obsidian. The frontmatter `title` names the note and its `tags` become node sets (`obsidian:tag:engineering`).

### Step 4: Sync Again

```python theme={null}
        print("\n=== Second sync ===")
        await sync_and_ask(vault)
```

Every sync walks the whole vault, but a note whose text did not change costs no extraction, so only the two edited notes are built again. The edited notes replace their old documents and the deleted note is forgotten, so the old answer — Ada Lovelace leads Project Atlas — does not come back.

## Additional Information

> **Syncing Your Own Vault**
>
> Pass your vault's folder as `vault_path`, and `folders=[...]` to read only some folders. Only regular `*.md` files are read: hidden files and folders (`.obsidian`, `.trash`, `.git`), attachments, and symbolic links are skipped. A renamed or moved note is a new document under its new path, and the old one is forgotten.

> **When a Sync Fails**
>
> If the vault folder is gone, or is empty although notes were synced from it before (a drive that did not mount), the sync raises `ObsidianVaultUnavailableError`; a folder that cannot be listed raises `ObsidianListingError`. Either way nothing is synced and nothing is forgotten. A note that cannot be read keeps what memory already holds and is counted in `cognee_sync_stats["failed"]`, so check it after a normal return.

> **Privacy**
>
> The text of every note, frontmatter included, goes to your configured LLM and embedding providers. Nothing is read until you build a source and run it. Keep a vault in its own dataset so it is easy to inspect and forget. [`cognee.forget`](https://docs.cognee.ai/core-concepts/main-operations/forget) does not remove the staged copy of the text, so the next sync builds the notes again.

> **[dlt (Data Load Tool)](https://docs.cognee.ai/integrations/dlt-integration.md#obsidian-connector)**
>
> Every Obsidian connector parameter and error, and how forget-on-delete works

> **[Remember](https://docs.cognee.ai/core-concepts/main-operations/remember.md)**
>
> All `remember()` parameters, including the dlt options

> **[Recall](https://docs.cognee.ai/core-concepts/main-operations/recall.md)**
>
> Query the synced vault with other search types

> **[Forget: Delete Agent Memory](https://docs.cognee.ai/core-concepts/main-operations/forget.md)**
>
> Wipe the vault dataset when you are done


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.