DuckDB & MotherDuck

by Mehdi Ouazza
5
4
3
2
1
New Plugin

Description

This plugin has not been manually reviewed by Obsidian staff. Run DuckDB SQL inside notes. Freeze query results inline as markdown tables. Optional MotherDuck token for cloud queries.

Reviews

No reviews yet.

Stats

stars
downloads
0
forks
0
days
NaN
days
NaN
days
0
total PRs
0
open PRs
0
closed PRs
0
merged PRs
0
total issues
0
open issues
0
closed issues
0
commits

Latest Version

Invalid date

Changelog

README file from

Github

DuckDB & MotherDuck for Obsidian

Bring external data into your Obsidian notes via DuckDB SQL, then freeze the results as a plain markdown table so you and any agent reading the vault see query + result as one document.

Works entirely offline with local DuckDB WASM. Add a MotherDuck token to query your cloud data alongside, picking per code block which connection to use.

Why this and not Dataview?

Dataview is the go-to plugin for querying the vault itself — your frontmatter, tags, and links across notes. This plugin solves the opposite problem: pulling external data (CSV, Parquet, JSON, Excel, Iceberg, Delta, geospatial files, plus your MotherDuck cloud) into a note via DuckDB SQL, and joining across them when you want.

  • Use Dataview for "list every note tagged #project, sorted by created date."
  • Use this for "the latest revenue numbers from my warehouse, joined with a local expenses CSV."

Output is regular markdown wrapped in sentinel comments — so it diffs cleanly in git, renders in any editor (Neovim, VS Code, mobile previews), and stays readable to agents reading the vault.

When to use it

  • Embedding live numbers in a blog draft (e.g. SELECT count(*) FROM read_parquet('events.parquet')).
  • Quick exploration of a CSV/Parquet/Excel file in your data folder without leaving Obsidian.
  • Snapshotting a MotherDuck query into a weekly report note (with scheduled refresh).
  • Joining a local file (e.g. expenses CSV) with cloud tables in MotherDuck.

What it does

  • duckdb / motherduck code blocks: render a SQL panel in reading mode with a ▶ Run button and a connection badge showing which engine the block runs against.
  • Freeze: "Freeze query at cursor" or the 📌 button in reading mode runs the query and writes the result as a markdown table directly below the block, bracketed by sentinel comments.
  • Refresh: "Refresh all queries in this note" re-runs every SQL block and replaces its frozen output.
  • Scheduled refresh: pick a daily / weekly cadence per note via the per-block dropdown; the plugin sweeps once an hour while Obsidian runs and refreshes overdue notes automatically. Activity log + manual "Refresh now" button in plugin settings.
  • Plugin API: app.plugins.getPlugin('duckdb-motherduck').api.refreshFile(path) and .runQuery(sql, connection?) (where connection is "local" or "cloud", defaulting to "local"), so Claude Code or other agents can trigger refreshes via obsidian eval.

Freeze format

```motherduck
SELECT brand, SUM(revenue) FROM sales GROUP BY 1 ORDER BY 2 DESC LIMIT 10
```
<!-- md:cache hash=a3f847b2 conn=cloud ts=2026-04-24T14:22:00Z rows=10 -->

| brand | sum(revenue) |
| ----- | ------------ |
| acme  | 42000        |
| ...   | ...          |

<!-- md:cache-end -->

The sentinel carries a query hash, connection, timestamp, and row count. Refresh/freeze replaces the sentinel block below the query.

The frozen output is regular markdown — open the same note in Neovim, VS Code, or cat, and you see the table inline with the query. No custom rendering required, and any agent skimming the vault sees both the question and its answer as one document.

Connections

Each block picks its backend via the fence type. Both connections can be configured at once and used side-by-side in the same note.

Fence Backend Needs token Reaches cloud
```duckdb @duckdb/duckdb-wasm no no
```motherduck @motherduck/wasm-client yes yes

A rendered local block:

Rendered DuckDB block

A rendered cloud block:

Rendered MotherDuck block

Local DuckDB has three sub-modes, set via the Path to local DuckDB file setting:

  • :memory: (default), ephemeral in-memory database. Reset on every Obsidian restart.
  • A bare filename like notes.duckdb for a persistent database in browser-managed storage (Origin Private File System). Survives Obsidian restart, full read/write, lives outside your vault.
  • An absolute path like /Users/you/data.duckdb or C:\Users\you\data.duckdb to query an existing .duckdb file from disk. Read-only: writes succeed inside the worker but don't persist back to the file.

Install

The plugin isn't in the Obsidian community store yet — the review queue is deep, so until it lands the easiest path is BRAT.

  1. Install BRAT from Settings → Community plugins.
  2. Open BRAT's settings → Add beta plugin → paste the repository: motherduckdb/obsidian-duckdb-motherduck.
  3. BRAT will pull the latest tagged release and install it. Enable DuckDB & MotherDuck in Settings → Community plugins.

BRAT also keeps the plugin updated when new releases ship.

Manual

  1. Clone this repo.
  2. npm install && npm run build, produces main.js.
  3. Copy main.js, manifest.json, and styles.css into <your-vault>/.obsidian/plugins/duckdb-motherduck/.
  4. In Obsidian: Settings → Community plugins → enable DuckDB & MotherDuck.

Usage

Create code blocks tagged with the connection you want:

```duckdb
-- runs against your local DuckDB
SELECT 42 AS answer, now() AS ts
```

```duckdb
-- read a Parquet/CSV/JSON file from disk
SELECT category, SUM(amount) AS total
FROM read_csv('/Users/you/data/expenses.csv')
GROUP BY 1
ORDER BY total DESC
LIMIT 5
```

```motherduck
-- query MotherDuck and join with a local CSV in the same SQL
SELECT s.region, s.revenue, e.amount AS expense
FROM sales s
LEFT JOIN read_csv('/Users/you/data/expenses.csv') e USING (region)
ORDER BY s.revenue DESC
```

In reading mode each block shows its connection badge (DuckDB :memory: or MotherDuck), a Refresh: none / daily / weekly dropdown for scheduled refresh (see below), and the ▶ Run / 📌 Freeze buttons.

From the command palette:

  • Refresh all queries in this note, re-runs every block in the current note.
  • Refresh query at cursor, re-runs and re-freezes only the block the cursor is on. Bind a hotkey under Settings → Hotkeys for fast vim-mode iteration.
  • Freeze query at cursor, alias of "Refresh query at cursor" — kept for users who think of it as the first-time freeze action.
  • Clear freeze at cursor, removes the frozen result below the SQL block at the cursor (matching the Clear freeze button shown in the rendered panel when a frozen result is present).
  • Reset DuckDB/MotherDuck connections, drops both connections; useful after changing the path or token.

Settings

Plugin settings

  • DuckDB → Path to local DuckDB file: :memory: (default), an OPFS bare filename, or an absolute file path. See Connections above.
  • MotherDuck → Token: optional. Stored plaintext in the plugin's data.json (see Security below). Prefer a service account token for scoped, individually revocable access; or a personal access token for quick experimentation.
  • Scheduled refresh: see the next section.
  • General → Row cap: max rows rendered inline or written into a frozen table. The runtime stops scanning at rowCap + 1 rows and discards the rest, so heavy queries (FROM 'huge.csv') don't materialize 40k rows in WASM heap just to throw 39 900 of them away. A truncation notice is appended if more rows existed.
  • General → Cell character cap: max characters per cell in rendered and frozen tables; longer values are truncated with an ellipsis. Hover a truncated cell in the live result to see the full value. Default 80.

Scheduled refresh

Pick a cadence in the Refresh dropdown above any SQL block to opt that note in for auto-refresh. The plugin writes a duckdb-motherduck-refresh: daily | weekly property to the note's frontmatter:

---
duckdb-motherduck-refresh: daily
duckdb-motherduck-refresh-last: 2026-05-04T10:30:00Z   # plugin-managed
---

While Obsidian is running and the Auto-refresh scheduled notes toggle is on, the plugin sweeps once an hour. Notes whose last - now exceeds their cadence get their frozen tables re-materialized. The active editor is skipped to avoid stomping in-progress edits.

After each sweep finishes, if Reset connections after each scheduled refresh is enabled (default on), the plugin terminates the DuckDB and MotherDuck WASM workers to free memory. The next interactive query pays a ~1–2 s init cost; in exchange, you don't carry materialized result sets between sweeps.

If a note errors three sweeps in a row with every block failing (zero blocks refreshed, errors recorded), the plugin auto-strips its duckdb-motherduck-refresh frontmatter so the hourly sweep stops poking it. Partial failures (some blocks succeed, some error) do not count — a working block keeps the schedule alive. Auto-unschedule events are written to the activity log.

The settings page also has:

  • A Refresh now button: forces a sweep of every note in the vault that has a SQL block, regardless of cadence or frontmatter opt-in. Useful before reading a dashboard, or for one-shot refreshes.
  • An Unschedule all button: strips duckdb-motherduck-refresh (and the plugin-managed -last timestamp) from every note's frontmatter. Use to bulk-disable auto-refresh after experimenting, or to free up the hourly sweep before running heavy queries.
  • An Activity log showing the last 100 refresh attempts (timestamp, trigger, path, blocks refreshed, first error message if any). Click a path to open the note. Clear log wipes history.

Agent trigger

Both the human button and the agent flow share the same code path. From a shell with the official Obsidian CLI installed:

obsidian eval code="app.plugins.getPlugin('duckdb-motherduck').api.refreshFile('path/to/note.md')"

...or wire it into a Claude Code skill. The plugin reports the number of blocks refreshed.

Build from source

npm install
npm test         # automated unit tests for parser/cache/table helpers
npm run build     # production bundle, main.js
npm run dev       # watch mode, rebuilds on save

main.js ends up around 2 MB because the local DuckDB WASM worker script is bundled inline. The .wasm binary itself is fetched from jsDelivr at runtime (see Remote assets).

Network access

The plugin makes network calls only in response to actions you take. There is no telemetry, no analytics, no calls to motherduck.com or any third-party for any reason other than fulfilling a SQL query or fetching a WASM runtime you triggered.

Concretely:

When What Where Sends
First time a duckdb block runs DuckDB WASM binary (~7 MB gzipped), once, then browser-cached https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@<version>/dist/duckdb-eh.wasm nothing — public CDN GET
First time a motherduck block runs MotherDuck WASM extension + DuckDB worker bundle https://app.motherduck.com/duckdb-wasm-assets/<version>/ nothing — public assets GET
Every motherduck block run (including scheduled refresh) Your SQL query and your token Your MotherDuck workspace your SQL + your token
Scheduled refresh sweep (when enabled) An hourly sweep checks frontmatter-opted-in notes and re-runs their blocks. Only motherduck blocks make network calls; duckdb blocks stay local. The sweep itself does no network I/O — it walks the vault and triggers blocks. Same as the row above (only motherduck blocks touch the network) Same as above

Scheduled refresh is off by default and opted into per-note via the Refresh: daily/weekly dropdown above any SQL block, which writes duckdb-motherduck-refresh: daily|weekly to that note's frontmatter. Toggle it off globally in Settings → Auto-refresh scheduled notes.

Requirements

  • Obsidian 1.5+
  • Desktop (tested on macOS; Windows/Linux expected to work, end-to-end testing pending). On mobile: :memory: and OPFS modes should work; absolute-path mode requires Node integration not available on mobile.
  • Internet connection on first use (to download wasm assets).

Security

The MotherDuck token, if set, is stored plaintext in <vault>/.obsidian/plugins/duckdb-motherduck/data.json. Don't commit that file. Don't sync your vault publicly with a token in it. Keychain integration isn't implemented; this matches the Obsidian-plugin-ecosystem norm (no plugin SDK API for encrypted secrets, no native deps shipped via the community store).

Queries run locally (duckdb blocks) or against your MotherDuck account (motherduck blocks). No telemetry is sent by the plugin.

Known limitations

  • No mobile validation, the architecture should work in mobile Obsidian for :memory: and OPFS modes, but hasn't been tested on iOS/Android. Absolute-path mode requires Node integration which isn't available on mobile.
  • Read-only for on-disk files, pointing at a real .duckdb file lets you query it, but writes (CREATE / INSERT / UPDATE) succeed only inside the worker and don't persist back to the file.
  • Scheduled refresh runs only while Obsidian is open. If you want notes refreshed while your laptop is asleep or Obsidian is closed, you need an external trigger (e.g. cron + the Obsidian CLI calling the plugin's API).
  • No keychain integration for the MotherDuck token, stored plaintext in data.json. See Security.

License

MIT. See LICENSE.