| name | obsidian-plugin-development |
| description | Build, modify, and debug Obsidian plugins using the TypeScript API. Use this skill when working with Obsidian plugin source code, the obsidian npm package, plugin UI (views, modals, settings, commands, ribbons), vault file operations, editor manipulation, workspace management, metadata cache, events, markdown rendering, or the Obsidian CLI. Covers plugin lifecycle, best practices, common patterns, and the full TypeScript API surface. |
| metadata | {"author":"obsidian-gemini","version":"1.0"} |
| compatibility | Requires Node.js, npm, and TypeScript. Targets the obsidian npm package. |
Obsidian Plugin Development
When to use this skill
Use this skill when:
- Creating or modifying an Obsidian plugin
- Working with the
obsidian npm package TypeScript API
- Building plugin UI: views, modals, settings tabs, commands, ribbon icons, context menus, status bar
- Performing vault file operations (read, create, modify, delete, rename)
- Manipulating the editor (cursor, selection, transactions)
- Working with the workspace (leaves, tabs, splits, sidebars)
- Using MetadataCache for frontmatter, links, tags, headings
- Handling events (vault, workspace, editor, metadata changes)
- Rendering markdown programmatically
- Making HTTP requests from a plugin (use
requestUrl, not fetch)
- Using the Obsidian CLI for automation, scripting, or developer tooling
- Debugging or testing an Obsidian plugin
Plugin anatomy
An Obsidian plugin consists of:
my-plugin/
├── main.ts # Entry point: default export extending Plugin
├── manifest.json # Plugin metadata (id, name, version, minAppVersion)
├── styles.css # Optional: plugin styles
├── package.json # npm dependencies
├── tsconfig.json # TypeScript config
└── esbuild.config.mjs # Build config (or rollup/vite)
manifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "Description of plugin",
"author": "Author Name",
"authorUrl": "https://example.com",
"isDesktopOnly": false
}
Main plugin class
import { Plugin } from 'obsidian';
export default class MyPlugin extends Plugin {
settings: MySettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new MySettingTab(this.app, this));
this.addCommand({ id: 'my-cmd', name: 'My Command', callback: () => {} });
this.addRibbonIcon('icon-name', 'Tooltip', () => {});
this.registerView('view-type', (leaf) => new MyView(leaf));
this.registerEvent(this.app.vault.on('modify', (file) => {}));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
Core API classes
The app object (available as this.app in Plugin subclasses) provides access to the entire API:
| Property | Class | Purpose |
|---|
app.vault | Vault | File CRUD operations |
app.workspace | Workspace | Panes, tabs, views, layout |
app.metadataCache | MetadataCache | Cached file metadata, link resolution |
app.fileManager | FileManager | High-level file operations, frontmatter |
Vault - File operations
const content = await this.app.vault.read(file);
const cached = await this.app.vault.cachedRead(file);
const newFile = await this.app.vault.create('path/file.md', 'content');
await this.app.vault.createFolder('path/folder');
await this.app.vault.modify(file, 'new content');
await this.app.vault.append(file, '\nappended text');
await this.app.vault.process(file, (data) => {
return data.replace('old', 'new');
});
await this.app.vault.delete(file);
await this.app.vault.trash(file, true);
await this.app.vault.rename(file, 'new/path.md');
const file = this.app.vault.getAbstractFileByPath('notes/note.md');
if (file instanceof TFile) {
}
const allMd = this.app.vault.getMarkdownFiles();
const allFiles = this.app.vault.getFiles();
IMPORTANT: Always use vault.getAbstractFileByPath() and check instanceof TFile/TFolder. Never construct TFile/TFolder objects directly.
Workspace - Views, leaves, layout
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
const file = this.app.workspace.getActiveFile();
const leaf = this.app.workspace.getLeaf('tab');
await leaf.openFile(file);
await this.app.workspace.openLinkText('Note Name', '', true);
const leaves = this.app.workspace.getLeavesOfType('my-view');
this.app.workspace.iterateAllLeaves((leaf) => {
});
const rightLeaf = this.app.workspace.getRightLeaf(false);
this.app.workspace.onLayoutReady(() => {
});
MetadataCache - Parsed file metadata
const cache = this.app.metadataCache.getFileCache(file);
if (cache) {
cache.headings;
cache.tags;
cache.links;
cache.embeds;
cache.frontmatter;
cache.sections;
}
const target = this.app.metadataCache.getFirstLinkpathDest('Note', 'source/path.md');
FileManager - High-level file operations
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm.tags = ['tag1', 'tag2'];
fm.modified = new Date().toISOString();
});
await this.app.fileManager.renameFile(file, 'new/path.md');
Editor - Text manipulation
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
const editor = view.editor;
const cursor = editor.getCursor();
const selection = editor.getSelection();
editor.replaceSelection('replacement');
editor.setSelection(anchor, head);
const value = editor.getValue();
const line = editor.getLine(lineNum);
editor.replaceRange('text', from, to);
editor.transaction({ changes: [{ from, to, text: 'new' }] });
editor.scrollIntoView({ from, to }, true);
}
UI components
Commands
this.addCommand({
id: 'unique-command-id',
name: 'Human-readable name',
callback: () => {
},
editorCallback: (editor: Editor, view: MarkdownView) => {
},
checkCallback: (checking: boolean) => {
if (canRun) {
if (!checking) {
doStuff();
}
return true;
}
return false;
},
hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'p' }],
});
Modals
class MyModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
this.setTitle('My Modal');
this.contentEl.createEl('p', { text: 'Content here' });
new Setting(this.contentEl).setName('Option').addToggle((t) => t.setValue(true).onChange((v) => {}));
}
onClose() {
this.contentEl.empty();
}
}
Settings tabs
class MySettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting name')
.setDesc('Description')
.addText((text) =>
text
.setPlaceholder('placeholder')
.setValue(this.plugin.settings.value)
.onChange(async (v) => {
this.plugin.settings.value = v;
await this.plugin.saveSettings();
})
);
}
}
Custom views
class MyView extends ItemView {
getViewType() { return 'my-view-type'; }
getDisplayText() { return 'My View'; }
getIcon() { return 'bot'; }
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.createEl('h4', { text: 'My View' });
this.addAction('refresh-cw', 'Refresh', () => { });
}
async onClose() { }
}
this.registerView('my-view-type', (leaf) => new MyView(leaf));
async activateView() {
let leaf = this.app.workspace.getLeavesOfType('my-view-type')[0];
if (!leaf) {
leaf = this.app.workspace.getRightLeaf(false)!;
await leaf.setViewState({ type: 'my-view-type', active: true });
}
this.app.workspace.revealLeaf(leaf);
}
Other UI
this.addRibbonIcon('dice', 'Tooltip text', (evt) => {});
const statusEl = this.addStatusBarItem();
statusEl.setText('Status text');
new Notice('Message to user');
new Notice('Persistent message', 0);
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
menu.addItem((item) => {
item
.setTitle('My action')
.setIcon('icon')
.onClick(() => {});
});
})
);
this.registerEvent(
this.app.workspace.on('editor-menu', (menu, editor, info) => {
menu.addItem((item) => {
item.setTitle('Editor action').onClick(() => {});
});
})
);
const div = containerEl.createDiv({ cls: 'my-class', text: 'Hello' });
const el = containerEl.createEl('span', { cls: 'highlight', attr: { 'data-id': '1' } });
el.empty();
el.addClass('new-class');
el.toggleClass('active', true);
import { setIcon } from 'obsidian';
setIcon(element, 'lucide-icon-name');
Events
Always use this.registerEvent() to ensure automatic cleanup on plugin unload:
this.registerEvent(this.app.vault.on('create', (file) => {}));
this.registerEvent(this.app.vault.on('modify', (file) => {}));
this.registerEvent(this.app.vault.on('delete', (file) => {}));
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {}));
this.registerEvent(this.app.workspace.on('file-open', (file) => {}));
this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {}));
this.registerEvent(this.app.workspace.on('layout-change', () => {}));
this.registerEvent(this.app.workspace.on('editor-change', (editor, info) => {}));
this.registerEvent(this.app.metadataCache.on('changed', (file, data, cache) => {}));
this.registerEvent(this.app.metadataCache.on('resolved', () => {}));
this.registerInterval(window.setInterval(() => {}, 60000));
this.registerDomEvent(document, 'click', (evt) => {});
Network requests
Always use requestUrl instead of fetch for cross-platform compatibility (desktop + mobile):
import { requestUrl } from 'obsidian';
const response = await requestUrl({
url: 'https://api.example.com/data',
method: 'POST',
headers: { Authorization: 'Bearer TOKEN' },
contentType: 'application/json',
body: JSON.stringify({ key: 'value' }),
});
const data = response.json;
const text = response.text;
const status = response.status;
Markdown rendering
import { MarkdownRenderer } from 'obsidian';
await MarkdownRenderer.render(
this.app,
'# Heading\nSome **bold** and a [[link]].',
containerEl,
'',
this
);
Key best practices
- Always use Obsidian API over low-level alternatives:
vault.getMarkdownFiles() not vault.adapter.list(), fileManager.processFrontMatter() not manual YAML parsing, app.metadataCache not re-parsing files
- Register everything for cleanup: Use
this.registerEvent(), this.registerInterval(), this.registerDomEvent(), this.addCommand(), this.addSettingTab(), this.registerView() - they all auto-clean on unload
- Use
requestUrl not fetch: Required for mobile compatibility
- Wait for layout: Use
this.app.workspace.onLayoutReady() before accessing UI
- Check
instanceof: Always verify TFile vs TFolder when using getAbstractFileByPath()
- Use CSS variables: Use Obsidian's theme variables for styling, test in both light and dark themes
- Use
setIcon(): Use import { setIcon } from 'obsidian' with Lucide icon names
- Debounce frequent operations: Use
import { debounce } from 'obsidian' for editor changes or API calls
- Use normalized paths: Obsidian uses forward slashes on all platforms
- Never block the main thread: Use
async/await for file operations, API calls, and heavy computations
Obsidian CLI
Obsidian 1.12+ includes a CLI for terminal-based vault interaction. See the CLI reference for complete command documentation.
Key developer commands:
obsidian dev:eval code="expression" - Execute JavaScript in the app console
obsidian dev:console - View captured console messages
obsidian dev:dom query="selector" - Query DOM elements
obsidian dev:css query="selector" - Inspect CSS with source locations
obsidian dev:screenshot - Capture app screenshots
obsidian dev:mobile - Toggle mobile emulation
obsidian dev:errors - Display captured errors
obsidian plugin:reload - Reload plugins during development
Further reading