Handle Obsidian events and workspace callbacks for plugin development.
Use when implementing reactive features, handling file changes,
or responding to user interactions in your plugin.
Trigger with phrases like "obsidian events", "obsidian callbacks",
"obsidian file change", "obsidian workspace events".
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Handle Obsidian events and workspace callbacks for plugin development.
Use when implementing reactive features, handling file changes,
or responding to user interactions in your plugin.
Trigger with phrases like "obsidian events", "obsidian callbacks",
"obsidian file change", "obsidian workspace events".
allowed-tools
Read, Write, Edit
version
1.13.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","obsidian","react"]
compatibility
Designed for Claude Code, also compatible with Codex and OpenClaw
Obsidian Webhooks & Events
Overview
Complete guide to Obsidian's event system: vault events (create, modify, delete, rename), workspace events (layout, leaf changes, editor state), metadataCache events, DOM events, custom EventRef patterns, and periodic tasks. Every event registration uses this.registerEvent() for automatic cleanup on plugin unload.
Prerequisites
Working Obsidian plugin with onload() / onunload() lifecycle
Understanding of TypeScript event handler signatures
Familiarity with Obsidian's TFile, TFolder, and WorkspaceLeaf types
Instructions
Step 1: Vault Events — File Lifecycle
Vault events fire when files and folders are created, modified, deleted, or renamed.
// File content modified (fires on save and on every sync update)
this
registerEvent
this
app
vault
on
'modify'
(file: TAbstractFile) =>
if
instanceof
TFile
this
onFileModified
// File deleted
this
registerEvent
this
app
vault
on
'delete'
(file: TAbstractFile) =>
if
instanceof
TFile
this
removeFromIndex
path
// File renamed or moved (includes folder moves)
this
registerEvent
this
app
vault
on
'rename'
(file: TAbstractFile, oldPath: string) =>
if
instanceof
TFile
this
updatePathReferences
path
Note: modify fires on every keystroke during live editing in some configurations. Always debounce if your handler does non-trivial work (see obsidian-rate-limits).
Step 2: Workspace Events — UI State Changes
Workspace events track what the user is looking at and how the UI layout changes.
asynconload() {
// Active file changed (user clicked a different tab/pane)this.registerEvent(
this.app.workspace.on('active-leaf-change', (leaf) => {
if (leaf) {
const view = leaf.view;
if (view.getViewType() === 'markdown') {
const file = (view asany).fileasTFile;
if (file) {
this.onActiveFileChanged(file);
}
}
}
})
);
// File opened in any pane (fires even if already active)this.registerEvent(
this.app.workspace.on('file-open', (file: TFile | null) => {
if (file) {
this.trackRecentFile(file);
}
})
);
// Layout changed (panes split, closed, rearranged)this.registerEvent(
this.app.workspace.on('layout-change', () => {
this.updateSidebarState();
})
);
// Editor changed (cursor moved, selection changed, content edited)this.registerEvent(
this.app.workspace.on('editor-change', (editor, info) => {
// info is MarkdownView — gives you the file contextconst cursor = editor.getCursor();
this.onCursorMoved(cursor.line, cursor.ch);
})
);
// Window/pane resizedthis.registerEvent(
this.app.workspace.on('resize', () => {
this.adjustCustomViews();
})
);
// Wait for layout to be fully initialized before accessing panesthis.app.workspace.onLayoutReady(() => {
this.initializeWithCurrentState();
});
}
Step 3: MetadataCache Events — Content Indexing
The metadataCache parses frontmatter, links, tags, and headings in the background. These events fire when parsing completes.
asynconload() {
// Single file's metadata changed (fires after modify, once parsing is done)this.registerEvent(
this.app.metadataCache.on('changed', (file: TFile, data: string, cache: CachedMetadata) => {
// cache contains parsed frontmatter, links, tags, headingsconst tags = cache.tags?.map(t => t.tag) ?? [];
const links = cache.links?.map(l => l.link) ?? [];
this.updateFileIndex(file.path, { tags, links });
})
);
// All files in vault have been indexed (fires once after startup)this.registerEvent(
this.app.metadataCache.on('resolved', () => {
console.log('Metadata cache fully resolved — safe to query all files');
this.buildFullIndex();
})
);
}
privatebuildFullIndex() {
const files = this.app.vault.getMarkdownFiles();
for (const file of files) {
const cache = this.app.metadataCache.getFileCache(file);
if (cache) {
this.updateFileIndex(file.path, {
tags: cache.tags?.map(t => t.tag) ?? [],
links: cache.links?.map(l => l.link) ?? [],
headings: cache.headings?.map(h => h.heading) ?? [],
frontmatter: cache.frontmatter,
});
}
}
}
The resolved event is critical for plugins that build indexes — querying metadataCache before it fires returns incomplete data.
Step 4: DOM Events with registerDomEvent
For custom UI elements, use registerDomEvent instead of raw addEventListener. Obsidian auto-removes these on plugin unload.
asynconload() {
// Register click handler on a custom elementconst button = this.addStatusBarItem();
button.setText('Click me');
this.registerDomEvent(button, 'click', (evt: MouseEvent) => {
newNotice('Status bar clicked!');
});
// Listen for keyboard shortcuts on the documentthis.registerDomEvent(document, 'keydown', (evt: KeyboardEvent) => {
if (evt.ctrlKey && evt.key === 'q') {
this.toggleFeature();
}
});
// Drag and drop on a custom viewconst dropZone = createEl('div', { cls: 'my-drop-zone' });
this.registerDomEvent(dropZone, 'dragover', (evt: DragEvent) => {
evt.preventDefault();
dropZone.addClass('drag-active');
});
this.registerDomEvent(dropZone, 'drop', async (evt: DragEvent) => {
evt.preventDefault();
dropZone.removeClass('drag-active');
const files = evt.dataTransfer?.files;
if (files?.length) {
awaitthis.handleDroppedFiles(files);
}
});
}
Step 5: Periodic Tasks with registerInterval
Use registerInterval for timers — they auto-clear on unload. Never use raw setInterval.
asynconload() {
// Auto-save draft every 30 secondsthis.registerInterval(
window.setInterval(() => {
this.autoSaveDraft();
}, 30_000)
);
// Refresh external data every 5 minutesthis.registerInterval(
window.setInterval(() => {
this.refreshExternalData();
}, 5 * 60_000)
);
}
private draftSaving = false;
privateasyncautoSaveDraft() {
// Overlap guard — skip if previous save is still runningif (this.draftSaving) return;
this.draftSaving = true;
try {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view?.file) {
const content = view.editor.getValue();
awaitthis.saveDraft(view.file.path, content);
}
} finally {
this.draftSaving = false;
}
}
Step 6: Custom Event Bus for Plugin-Internal Communication
For complex plugins with multiple views or components, create an internal event bus.
Obsidian's Events class is the same base class used by Vault, Workspace, and MetadataCache. Using it for your own bus gives you a consistent pattern with on/off/trigger.
Output
Vault event handlers for file create, modify, delete, rename
Workspace event handlers for active leaf, file open, editor changes, layout
MetadataCache handlers for content parsing and full-vault resolution
DOM event registration with auto-cleanup
Periodic tasks with overlap guards
Custom event bus for internal plugin communication