Obsidian Bases API for Plugins: What Works Now and What’s Still Missing
What the Obsidian Bases API Actually Gives You Right Now
Obsidian 1.10.0 shipped the first iteration of a public Bases API for plugin developers. If you were hoping for a clean app.bases.getViewFiles() call you can drop into any plugin, that doesn’t exist yet. What shipped is narrower: a mechanism for plugins to register custom view types inside Bases, not a general-purpose headless query runner.
That distinction matters a lot if you’re building something like a ranking tool or a cohort picker that wants to consume a Base result without surfacing a view to the user.
The registerBasesView() entry point
Call this.registerBasesView() from your plugin’s onload() and Obsidian registers your type alongside its built-in table, board, and gallery views. Inside that view class you get structured, stable access to query results — which is genuinely useful once you accept the constraint it imposes.
Results live at this.data.data, typed as BasesEntry[]. Each entry represents one matching file. You read individual property values with entry.getValue(propertyId). Grouped results sit at this.data.groupedData. No leaf-settling race, no polling internal state — just a typed array ready to iterate.
class MyBasesView extends BasesView {
render() {
const entries = this.data.data; // BasesEntry[]
for (const entry of entries) {
const file = entry.file; // TFile
// process file here
}
}
}
The registerBasesView() call also returns false if Bases is disabled in the current vault, which gives you a clean way to guard capability at load time.
The public type surface in obsidian.d.ts
As of 1.10.x the relevant types are:
- BasesEntry — one row in the result set. Carries
file: TFileand agetValue(propertyId: string)method. - BasesQueryResult — the full output of a query run.
- BasesView — the abstract class your custom view extends.
- QueryController — executes queries and emits result updates.
What’s absent: any public method that runs a query against a user-specified .base file from outside a view. No app.bases.query(), no app.bases.getViewFiles(), nothing like it.
The gap: headless queries
The feature request that prompted this post asks for something conceptually simple. Given a .base file and a view name, return the matching notes:
const files = await app.bases.getViewFiles(baseFile, 'To Read');
// Promise<TFile[]>
That call doesn’t exist. A plugin that wants Base results today has two real options.
Option one: implement a BasesView and access this.data.data from within it. Clean and stable, but it forces your plugin into the view paradigm even when you have no UI to show.
Option two — the current workaround — open the .base file in a hidden leaf, wait for the QueryController to emit results, then read internal state and close the leaf:
const leaf = app.workspace.getLeaf(true);
await leaf.openFile(baseFile);
// wait for the controller to settle (poll or observe)
const controller = (leaf.view as any).controller;
const files = controller.results.map((e: any) => e.file);
leaf.detach();
This reaches into undocumented internal state that can change without notice. It spins up a full leaf even if the result is never shown. It is the definition of fragile, and the fact that real plugins are shipping with this code is a sign that the API surface needs to expand.
Why this matters for the broader plugin ecosystem
Bases is meant to be Obsidian’s native way to define a cohort of notes. A user who maintains a Base called Books.base with a view named To Read has already done the filtering work. Without a headless API, every plugin that wants to operate on a user-defined set of notes has to re-implement its own filter system — folders, tags, Dataview queries, plugin-specific query languages.
The practical cost: users maintain parallel definitions of the same set across multiple plugins. Dataview could express LIST FROM "Books.base#To Read" as naturally as it expresses a folder source. Tasks could scope task searches to a Base view. Spaced Repetition decks and Projects workspaces could both source from the same Base definition instead of requiring separate folder structures. Each plugin that can’t read a Base programmatically is asking users to duplicate their filtering logic.
What a richer API might look like
The minimal useful form is returning TFile[] — enough for plugins that only need file identity. A slightly richer version returns BasesEntry[] so callers can also read property values:
// minimal
app.bases.getViewFiles(baseFile: TFile, viewName: string): Promise<TFile[]>
// richer
app.bases.getViewEntries(baseFile: TFile, viewName: string): Promise<BasesEntry[]>
The underlying infrastructure already exists. QueryController runs this query every time a Base is opened. The missing piece is a stable public path to invoke it outside a view context — something the team could expose without fundamentally changing how Bases works internally.
Status as of mid-2026
The forum request is open. The 1.10 API was a real step forward — custom view types with structured result access are a solid foundation — but the headless query surface hasn’t shipped. If your plugin needs this, the most direct signal you can send is a vote on the feature thread. In the meantime, the registerBasesView() path is the cleanest option available if you can tolerate building a view around your logic.
Sources
