Open-source project
obsidianmd/obsidian-api avatar
obsidianmd/obsidian-api

obsidian-api: the type definitions Obsidian plugins compile against

Type definitions for the latest Obsidian API.

2,329 stars270 forksUnknownMIT

At a glance

What is it?
This repository publishes the npm package named obsidian, which contains no runtime code at all: its main field is empty and its types field points at a declaration file, so a plugin imports it for the editor to supply at runtime. Version 1.13.2 ships today, and the lifecycle contract matters, since registering events, DOM listeners and intervals through the right helper is what keeps a plugin from leaking after an unload.
Who is it for?
Adopt this package if you write Obsidian plugins in TypeScript, since it is the definition of the API surface your code compiles against and there is no substitute for it: the runtime is supplied by the app at runtime, which is why the main field is empty. Do not expect it to work as a library outside Obsidian, and do not treat it as a stable contract, because the declarations track the latest app version rather than a compatibility range.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 90 days ago.
What is it written in?
GitHub does not report a main language for this repository.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A package with no code in it

The most surprising thing about this repository is how little is in it. The top level holds a README, a change history file, the terms text, a package.json and three declaration files: obsidian.d.ts, canvas.d.ts and publish.d.ts.

The package.json is where the design becomes clear. The package is named obsidian at version 1.13.2, and its main field is an empty string while its types field points at obsidian.d.ts. In other words, there is no JavaScript to require. The dependency exists so that require('obsidian') resolves for your bundler and for your editor's type checker, and the actual implementation is injected by the Obsidian application when your plugin runs inside it.

That is why the README describes plugin structure in terms of main.js rather than a package entry point. Your plugin is a bundle, and its dependency on obsidian is resolved by the host, not installed as a library you can call from a script.

The practical consequences are worth stating plainly. You cannot unit test a plugin by requiring the obsidian package in Node, because requiring it yields nothing. You can type-check against it fully, and that is what it is for. And a mismatch between the declarations you compile against and the app version a user runs shows up as a runtime failure, not as a compile error, which is why minAppVersion exists.

The canvas and publish declaration files are separate because those APIs are optional surfaces: a plugin that never touches canvas or publishing does not need them.

manifest.json, and the two fields that gate loading

The plugin manifest is where a plugin declares what it is, and the README enumerates its fields in a way that reads as a contract.

There are the obvious ones: id, name, author and version for identity, and description for the long text shown to users. Then there are three that matter more than they look.

minAppVersion is the minimum Obsidian version your plugin requires. It is the field that stops your plugin loading on an older app, and getting it wrong in the other direction means your plugin loads and then fails on an API that did not exist yet.

isDesktopOnly declares whether your plugin uses NodeJS or Electron APIs. That is a hard boundary rather than a preference: a plugin that reads the filesystem with require('fs') cannot run on mobile, and a plugin that declares isDesktopOnly will simply not appear on a phone.

Two optional fields round it out. authorUrl points at the author's own site, and fundingUrl is a donation link that the app surfaces, with the README noting the intent is to show appreciation and support plugin development.

Alongside the manifest, main.js is the entry point. The README says it must export a default class extending Plugin, must import the Obsidian API through require('obsidian'), may import NodeJS or Electron APIs through require('fs') or require('electron'), and must bundle all external dependencies into the file using Rollup, Webpack or another bundler.

That last requirement is the one that surprises people: a plugin ships one bundled file, not a dependency tree. The type package in this repository is a compile-time dependency, and everything else has to end up inside main.js.

The object graph: App, Vault, Workspace, MetadataCache

The README describes the application as a few major modules, reachable through the global App object, which you access as this.app inside a plugin.

Vault is the interface for files and folders in the vault, so anything to do with reading, writing, creating or searching notes goes through it. Workspace is the interface for panes on screen, which covers views, leaves and layout, and it is the module you reach for when a plugin needs to know what the user is currently looking at.

MetadataCache is the one that saves the most work. It contains cached metadata about each markdown file, and the README names the kinds: headings, links, embeds, tags and blocks. Reading that cache rather than parsing files yourself is the difference between a plugin that opens instantly and one that re-parses a large vault on every load.

The declarations are versioned alongside the application, and the package carries a change history, so you can see what moved between two of them before you bump your minimum version.

A minimal plugin therefore comes down to three decisions: which API version you declare support for, which of the four interfaces you read from, and which registration helper you use for each side effect you add.

Three of those metadata kinds deserve a note. Links and embeds give you the note graph without parsing wikilinks yourself. Blocks give you per-block reference information. And tags are the metadata your users already write, so a tag-based plugin can find its subjects without a convention of its own.

The App interface provides accessors to these interfaces, which means the shape is a small object graph rather than a flat namespace of functions. That is why event registration takes a reference to app or workspace and returns an event reference you pass to registerEvent.

Plugin helpers and loadData, the two halves of a plugin

Inheriting from Plugin gives you a set of registration helpers, and the README lists them as the practical surface of the base class.

this.addRibbonIcon adds an icon to the ribbon, which is how a plugin becomes visible in the user interface. this.addStatusBarItem adds an element to the status bar at the bottom. this.addCommand registers a global command with an optional default hotkey. this.addSettingTab registers a settings tab. this.registerView registers a new kind of view. And this.loadData with this.saveData handles plugin data persistence.

That last pair deserves more attention than the README gives it. It is the plugin's own key-value store, separate from the vault, and it is where a plugin keeps its settings. It is also the answer to a question every plugin author asks: settings do not belong in a note, because notes are the user's content and are synced, shared and occasionally version-controlled.

The command surface is the other one to think about. A global command with a default hotkey is discoverable, since users can find commands they have never seen through the command palette, while a hotkey you register but do not document is invisible to everyone else.

Together these helpers mean a minimal plugin is close to empty: register a command, register a view or a ribbon icon, read settings from loadData on load and write them with saveData, and ship a manifest describing it.

Registering events, DOM events and intervals correctly

This is the part of the README that is a genuine engineering contract rather than a feature list, and getting it wrong produces a bug that is hard to see.

For events from the API's own event interfaces, such as App and Workspace, the README says to use this.registerEvent:

javascript
this.registerEvent(app.on('event-name', callback));

The stated reason is that registerEvent automatically detaches your handler when the plugin unloads. For DOM events on elements that persist on the page after unload, such as window or document events, the equivalent is registerDomEvent, which takes the element, the event name and the callback. And for setInterval, the README says to use registerInterval, wrapping the interval id in the call.

Three helpers, one purpose: making the plugin's lifetime explicit so the app can clean up. That matters because Obsidian plugins can be disabled and enabled repeatedly within a session, and a plugin that binds a document listener directly accumulates a duplicate handler every time it loads. The symptom is a plugin that gets slower and duplicates its side effects the more you toggle it, which is exactly the kind of report that is hard to trace back to a missing wrapper.

Using the right helper is also how you avoid cleaning up by hand, which would require tracking every registration and detaching it in an unload callback, and which is the kind of bookkeeping that gets forgotten when a new event source is added.

Exact version pins, and why they are exact

Two details in the package metadata affect a plugin author's dependency choices.

The first is exact versions. The runtime dependencies are @types/codemirror at 5.60.8 and moment at 2.30.1, and the peer dependencies are @codemirror/state at 6.7.0 and @codemirror/view at 6.43.5, all pinned to a specific patch rather than a range. For a type package that must line up with what the app itself bundles, pinning is the right call, since a range would let your build compile against a CodeMirror version the app is not running.

The cost is on your side. If the app moves to a newer CodeMirror and you upgrade the declarations, you may find yourself resolving a peer dependency conflict, and the plugin build is where you discover it. CodeMirror 5 types alongside CodeMirror 6 peers is also a deliberate signal about which era of the editor API the declarations describe.

Where to file issues and what to copy

The second detail is process. The README says issues with the API, and requests for new APIs, should go to the Obsidian forum in the developers API category, rather than to the repository's issue tracker. That matters for anyone hoping to file a bug against a wrong type: the wrong venue means a slower, less searchable answer.

For structure, the README points plugin authors at a separate sample plugin repository as the reference implementation, and the API documentation at docs.obsidian.md. The types are in this repository; the tutorial is elsewhere.

Nothing was committed to the repository after 2026-07-14.

Editorial conclusion

Adopt this package if you write Obsidian plugins in TypeScript, since it is the definition of the API surface your code compiles against and there is no substitute for it: the runtime is supplied by the app at runtime, which is why the main field is empty. Do not expect it to work as a library outside Obsidian, and do not treat it as a stable contract, because the declarations track the latest app version rather than a compatibility range. Verify four things. Read manifest.json requirements before you write code, particularly minAppVersion and isDesktopOnly, since those two fields determine whether your plugin loads on a user's setup at all. Use the registration helpers rather than raw event binding, because registerEvent, registerDomEvent and registerInterval exist so handlers detach when the plugin unloads, and a plugin that binds window or document events directly will leak across disable and enable cycles. Pin your CodeMirror peer dependencies deliberately, since the package pins @codemirror/state and @codemirror/view to exact versions that will lag your editor's own bundled copies. And check the change history when the app updates, since the definitions move with it and no semantic versioning promise is made. Start by copying the obsidian-sample-plugin template rather than a bare project, because the manifest and bundling requirements are the parts people get wrong first.

Frequently asked questions

What does the obsidian npm package contain?

Only type definitions. Its package.json leaves the main field empty and points the types field at obsidian.d.ts, with separate declarations for canvas and publish, so the runtime implementation is supplied by the Obsidian app rather than installed.

Which manifest.json fields decide whether my plugin loads?

minAppVersion sets the minimum Obsidian version required, and isDesktopOnly declares whether the plugin uses NodeJS or Electron APIs, which excludes it from mobile. Both are load-time gates rather than preferences.

Why must I use registerEvent instead of binding events directly?

registerEvent, registerDomEvent and registerInterval detach their handlers automatically when the plugin unloads. Binding window or document events directly leaves handlers attached across disable and enable cycles, so handlers accumulate and the plugin's actions repeat.

Where should I store plugin settings?

In the plugin's own data store through this.loadData and this.saveData, not in a note in the vault. Settings tab registration is done with this.addSettingTab, and the README lists this alongside the ribbon icon, status bar item, command and view helpers.

How do I report a wrong type or request a new API?

The README directs API issues and requests for new APIs to the Obsidian forum in the developers API category, not to the repository. The sample plugin repository and docs.obsidian.md are the references for structure.

Official sources

  1. Issues
  2. License: MIT
  3. obsidianmd/obsidian-api on GitHub
  4. Project website
  5. README