Safoine El Khabich
dark
Toggle monthly spotlight
Back to blog
5 min read

Using OpenClaw's .openclaw folder as an Obsidian vault

OpenClaw stores everything in Markdown. Obsidian reads Markdown. So I made them the same folder — here's the architecture, the scripts, and what I learned.

ai obsidian productivity opensource

The problem

I’ve been running OpenClaw on my server for a few months — it handles WhatsApp, Telegram, Discord conversations through a single AI agent. The thing I like about it is that everything is local. The agent’s personality (SOUL.md), its memory (MEMORY.md), daily logs, session transcripts — all Markdown and JSONL files sitting in ~/.openclaw/.

Separately, I use Obsidian for personal knowledge management. Notes, project tracking, references — the usual.

At some point I realized I was maintaining two knowledge systems that didn’t talk to each other. The AI would have a conversation about a project, build up context in its memory files, and none of that would show up in my Obsidian vault. I’d take notes about a topic, and the AI had no way to reference any of it.

The fix was obvious once I saw it: ~/.openclaw is already mostly a Markdown folder. Just make it the Obsidian vault.

What OpenClaw’s folder looks like

For context, here’s what OpenClaw creates on disk:

~/.openclaw/
├── workspace/
│   ├── SOUL.md            # agent personality/tone
│   ├── MEMORY.md          # curated long-term memory (~100 lines)
│   ├── USER.md            # info about you
│   ├── IDENTITY.md        # agent name/identity
│   ├── AGENTS.md          # operating instructions
│   └── memory/
│       ├── 2026-02-22.md  # daily memory log
│       ├── people/        # per-person context
│       ├── projects/      # per-project context
│       └── decisions/     # decision logs
├── agents/
│   └── <agentId>/sessions/
│       ├── sessions.json  # session index
│       └── *.jsonl        # raw transcripts
├── skills/
├── credentials/
└── openclaw.json

All the workspace/ files are plain Markdown. The agent reads and writes them at runtime. Session transcripts are JSONL — one JSON object per line with messages, metadata, token usage.

The architecture

Add Obsidian structure around the existing OpenClaw files without moving or renaming anything. OpenClaw keeps writing to its usual paths. A couple of scripts bridge the gap.

flowchart TB
    subgraph inputs["Input channels"]
        WA[WhatsApp]
        TG[Telegram]
        DC[Discord]
    end

    subgraph openclaw["OpenClaw runtime"]
        SOUL[SOUL.md]
        MEM[MEMORY.md]
        DAILY[Daily memory logs]
        JSONL[JSONL sessions]
    end

    subgraph bridge["Bridge scripts"]
        CONV["convert-sessions.py\nJSONL → Markdown"]
        INJ["inject-frontmatter.sh\nAdd YAML metadata"]
        WATCH["inotifywait watcher\nsystemd service"]
    end

    subgraph obsidian["Obsidian layer"]
        DASH[Dataview dashboards]
        NOTES[Your notes / Zettelkasten]
        SESS[Converted session notes]
        MOCS[Maps of Content]
    end

    WA & TG & DC --> openclaw
    JSONL --> CONV --> SESS
    DAILY --> INJ
    WATCH --> CONV
    WATCH --> INJ
    SOUL & MEM & SESS & NOTES --> DASH
    NOTES & SESS --> MOCS

Three layers:

  1. OpenClaw native files — untouched, agent reads/writes them normally
  2. Bridge scripts — Python converts JSONL sessions to Markdown, shell script injects YAML frontmatter into workspace files so Dataview can query them
  3. Obsidian overlay — numbered folders for PARA + Zettelkasten, templates, Dataview dashboards

Vault structure

Number-prefixed folders so my stuff sorts above OpenClaw’s native directories:

~/.openclaw/                          ← vault root
├── .obsidian/                        ← obsidian config
├── 00-Dashboard/                     ← dataview-powered dashboards
├── 01-Inbox/                         ← fleeting captures
├── 02-Projects/                      ← active projects (PARA)
├── 03-Areas/                         ← ongoing domains
│   └── Career/ Health/ MLOps/ ...
├── 04-Resources/                     ← books, articles, courses
├── 05-Zettelkasten/                  ← atomic notes
├── 06-MOCs/                          ← maps of content
├── 07-Archive/                       ← done/inactive
├── 08-Templates/                     ← note templates
├── 09-Sessions/                      ← converted AI transcripts
│   └── 2026/02-Feb/
├── 10-Scripts/                       ← automation

├── workspace/                        ← openclaw native (untouched)
├── agents/                           ← openclaw native
├── skills/                           ← openclaw native
└── openclaw.json

Nothing fancy. 00-07 is a PARA-ish structure. 09-Sessions is where converted transcripts land. OpenClaw’s folders stay at the bottom.

The frontmatter schema

This is what makes cross-querying work. Every note — AI-generated or human — gets consistent YAML frontmatter:

---
created: 2026-02-22
type: session        # session | zettel | project | soul | memory-daily | moc
source: openclaw     # openclaw | human
tags: [session, telegram]
---

For sessions I also track channel, token counts, and cost:

agent: main
channel: telegram
tokens_in: 12450
tokens_out: 8920
cost: 0.032
session_id: abc123

The source field is how Dataview distinguishes “things the AI wrote” from “things I wrote.” The type field controls which dashboards pick up the note.

Converting sessions

OpenClaw stores conversations as JSONL. Each line is a message with role, content, timestamp, and usage metadata. The conversion script parses these into readable Markdown:

# core logic (simplified)
for line in jsonl_file:
    entry = json.loads(line)
    msg = entry.get("message", entry)
    role = msg.get("role", "unknown")
    content = extract_text(msg.get("content", ""))

    emoji = {"user": "👤", "assistant": "🤖", "system": "⚙️"}[role]
    messages.append(f"### {emoji} {role.title()} {time_str}\n\n{content}\n")

Output:

---
created: 2026-02-22
type: session
source: openclaw
channel: telegram
tokens_in: 12450
tokens_out: 8920
cost: 0.032
---

# Session — 2026-02-22 via Telegram

### 👤 User 14:32
Can you research Kubernetes security best practices?

---

### 🤖 Assistant 14:33
Based on the CIS benchmarks and recent CVEs...

## Key takeaways
-

## Links to vault notes
- [[]]

That “Links to vault notes” section is blank by default. When I review a session and spot something worth connecting, I add [[wikilinks]] to relevant project notes or concepts. This is the manual step that creates actual graph connections.

The script tracks converted files by hash so it’s safe to re-run. Output goes to 09-Sessions/YYYY/MM-Mon/.

Injecting frontmatter into workspace files

OpenClaw’s native Markdown files don’t have YAML frontmatter. A shell script adds it:

inject_if_missing() {
    local file="$1" type="$2" tags="$3"
    [ -f "$file" ] || return
    head -1 "$file" | grep -q '^---$' && return  # skip if exists

    tmp=$(mktemp)
    printf -- "---\ntype: %s\nsource: openclaw\ntags: [%s]\n---\n\n" \
        "$type" "$tags" > "$tmp"
    cat "$file" >> "$tmp"
    mv "$tmp" "$file"
}

inject_if_missing "$WORKSPACE/SOUL.md" "soul" "ai-brain, soul"
inject_if_missing "$WORKSPACE/MEMORY.md" "memory-curated" "ai-brain, memory"

OpenClaw doesn’t care about the frontmatter — it treats --- blocks as regular text. Obsidian parses them as metadata. Both tools happy.

The file watcher

A systemd user service watches for changes and triggers conversions:

inotifywait -m -r \
    -e create -e modify -e moved_to \
    --include '.*\.(jsonl|md)$' \
    "$OPENCLAW_DIR/agents/" \
    "$OPENCLAW_DIR/workspace/" | while read -r directory event filename; do

    if [[ "$filename" == *.jsonl ]]; then
        sleep 2  # wait for write to finish
        python3 convert-sessions.py
    fi

    if [[ "$directory" == *workspace/memory* ]]; then
        bash inject-frontmatter.sh
    fi
done

With debouncing. Cron runs every 15 minutes as fallback.

# ~/.config/systemd/user/openclaw-vault-sync.service
[Unit]
Description=OpenClaw Vault Sync Watcher

[Service]
Type=simple
ExecStart=/bin/bash ~/.openclaw/10-Scripts/watch-openclaw.sh
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user enable --now openclaw-vault-sync.service

Dataview dashboards

The home dashboard queries both AI and human content:

TABLE WITHOUT ID
  file.link AS "Session",
  channel AS "Channel",
  tokens_out AS "Tokens Out",
  cost AS "Cost ($)"
FROM "09-Sessions"
SORT created DESC
LIMIT 10

The AI Brain Dashboard shows the agent’s soul config, memory layers, people it knows about, project context, and session cost tracking. One page for everything the AI knows.

Daily notes embed the AI’s memory log using transclusion:

## AI activity today
> ![[2026-02-22#]]

My daily note shows what I did and what the AI did. Single record of the day.

The linking model

Three mechanisms connect AI content to human content:

flowchart LR
    subgraph m1["Wikilinks in workspace files"]
        A1[Edit MEMORY.md] --> A2["Add [[Project Name]] links"]
    end
    subgraph m2["Dataview queries"]
        B1[Dashboards query both zones] --> B2[Tables show AI + human content]
    end
    subgraph m3["MOCs as indexes"]
        C1[Maps of Content] --> C2["Reference workspace/ + vault notes"]
    end

The most impactful one: editing MEMORY.md to include [[wikilinks]]. When the AI writes “working on the infrastructure migration,” I change it to “working on the [[Infrastructure Migration]]”. OpenClaw sees plain text. Obsidian sees a link. The graph grows.

What I’d change

Session summaries are manual. The conversion script doesn’t generate TL;DRs. Could pipe sessions through an LLM for auto-summaries, but reading through them and annotating is part of how I process things.

The JSONL format varies. OpenClaw’s session format isn’t formally documented, so the parser handles a few different structures depending on version. Lots of get("field", fallback) chains.

No mobile access. Vault lives on my server. I access via Syncthing to my laptop. No good mobile story yet.

Frontmatter injection is one-way. If OpenClaw rewrites a workspace file from scratch, the frontmatter gets blown away. The cron job re-injects it, but there’s a window where Dataview might miss it. Not a real problem in practice.

Setup

Packaged everything into a setup script:

git clone https://github.com/safoine/openclaw-obsidian-vault
cd openclaw-obsidian-vault
bash setup.sh

Then:

sudo apt install -y inotify-tools
bash ~/.openclaw/10-Scripts/inject-frontmatter.sh
python3 ~/.openclaw/10-Scripts/convert-sessions.py --force
systemctl --user enable --now openclaw-vault-sync.service

Open ~/.openclaw as a vault in Obsidian, install Dataview + Templater + Calendar + Periodic Notes, done.

Plugins needed

PluginWhy
DataviewPowers every dashboard query
TemplaterTemplate variables in notes
CalendarSidebar calendar for daily notes
Periodic NotesDaily/weekly note creation
Obsidian GitOptional, auto git backup

Closing thoughts

The whole thing works because both tools operate on plain Markdown. No API integration, no sync service, no database bridge. Two tools reading and writing to the same folder, with a thin script layer to translate between JSONL and frontmattered Markdown.

The graph view after a few weeks is genuinely useful. Project notes connect to AI sessions. People notes link to what the AI remembers about them. Session notes link to the zettelkasten concepts they spawned.

If you’re running any local AI agent that writes to disk — the same approach applies. The scripts are OpenClaw-specific but the pattern is general: make the AI’s working directory your vault root, bridge the file formats, let Dataview do the rest.


Source code on GitHub — setup script, templates, dashboards, conversion scripts, systemd service.