Skip to main content
Obsidian’s event system allows plugins to react to changes in the vault, workspace, and other components. Understanding how to properly register and handle events is crucial for building responsive plugins.

Event Registration

The Component class (which Plugin extends) provides methods for registering events that are automatically cleaned up when the plugin unloads.

registerEvent()

Use registerEvent() to register event listeners on Obsidian objects:
this.registerEvent(
  this.app.workspace.on('file-open', (file) => {
    console.log('File opened:', file?.path);
  })
);
registerEvent() returns the EventRef, but you typically don’t need to store it. The Component system handles cleanup automatically.

registerDomEvent()

Use registerDomEvent() to register DOM event listeners:
// Document events
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
  console.log('Document clicked');
});

// Window events
this.registerDomEvent(window, 'resize', (evt: UIEvent) => {
  console.log('Window resized');
});

// Element events
const button = containerEl.createEl('button');
this.registerDomEvent(button, 'click', (evt: MouseEvent) => {
  console.log('Button clicked');
});
Only use registerDomEvent() for elements that persist after your plugin unloads (like window or document). For elements created by your plugin, regular event listeners are automatically cleaned up with the element.

registerInterval()

Use registerInterval() to register intervals that are automatically cleared:
this.registerInterval(
  window.setInterval(() => {
    console.log('Periodic check');
  }, 5000)
);
Always use window.setInterval() instead of setInterval() to avoid TypeScript confusion between Node.js and browser APIs.

Common Events

Workspace Events

The Workspace emits events related to UI changes:
// File opened
this.registerEvent(
  this.app.workspace.on('file-open', (file: TFile | null) => {
    if (file) {
      console.log('Opened:', file.path);
    }
  })
);

// Active leaf changed
this.registerEvent(
  this.app.workspace.on('active-leaf-change', (leaf: WorkspaceLeaf) => {
    console.log('Active leaf changed');
  })
);

// Layout changed
this.registerEvent(
  this.app.workspace.on('layout-change', () => {
    console.log('Layout changed');
  })
);

// Window opened (popout windows)
this.registerEvent(
  this.app.workspace.on('window-open', (win: WorkspaceWindow) => {
    console.log('New window opened');
  })
);

// Window closed (popout windows)
this.registerEvent(
  this.app.workspace.on('window-close', (win: WorkspaceWindow) => {
    console.log('Window closed');
  })
);

// Quick preview shown
this.registerEvent(
  this.app.workspace.on('quick-preview', (file: TFile, data: string) => {
    console.log('Quick preview:', file.path);
  })
);

Vault Events

The Vault emits events related to file system changes:
// File created
this.registerEvent(
  this.app.vault.on('create', (file: TAbstractFile) => {
    console.log('Created:', file.path);
  })
);

// File modified
this.registerEvent(
  this.app.vault.on('modify', (file: TAbstractFile) => {
    console.log('Modified:', file.path);
  })
);

// File deleted
this.registerEvent(
  this.app.vault.on('delete', (file: TAbstractFile) => {
    console.log('Deleted:', file.path);
  })
);

// File renamed
this.registerEvent(
  this.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => {
    console.log('Renamed:', oldPath, '->', file.path);
  })
);
Vault events are fired for all file types, not just markdown files. Use file instanceof TFile or check file.extension to filter.

MetadataCache Events

The MetadataCache emits events when cached metadata changes:
// Metadata changed
this.registerEvent(
  this.app.metadataCache.on('changed', (file: TFile) => {
    const cache = this.app.metadataCache.getFileCache(file);
    console.log('Metadata updated for:', file.path);
  })
);

// Cache resolved (on startup)
this.registerEvent(
  this.app.metadataCache.on('resolved', () => {
    console.log('Metadata cache fully loaded');
  })
);
The changed event is emitted asynchronously after a file is modified. Don’t rely on immediate metadata updates.

Event Patterns

Debouncing Events

For events that fire frequently, use debouncing to limit processing:
import { debounce } from 'obsidian';

export default class MyPlugin extends Plugin {
  async onload() {
    // Debounce the handler
    const debouncedHandler = debounce(
      (file: TFile) => {
        console.log('Processing:', file.path);
      },
      1000, // Wait 1 second
      true  // Reset timer on each call
    );
    
    this.registerEvent(
      this.app.vault.on('modify', (file) => {
        if (file instanceof TFile) {
          debouncedHandler(file);
        }
      })
    );
  }
}

Filtering Events

Filter events to only process relevant files:
this.registerEvent(
  this.app.vault.on('modify', (file) => {
    // Only process markdown files
    if (!(file instanceof TFile) || file.extension !== 'md') {
      return;
    }
    
    // Only process files in specific folder
    if (!file.path.startsWith('notes/')) {
      return;
    }
    
    // Process the file
    this.processFile(file);
  })
);

Combining Multiple Events

React to multiple related events:
const handleFileChange = async (file: TFile) => {
  console.log('File changed:', file.path);
  await this.updateIndex(file);
};

this.registerEvent(
  this.app.vault.on('create', (file) => {
    if (file instanceof TFile) {
      handleFileChange(file);
    }
  })
);

this.registerEvent(
  this.app.vault.on('modify', (file) => {
    if (file instanceof TFile) {
      handleFileChange(file);
    }
  })
);

this.registerEvent(
  this.app.vault.on('delete', (file) => {
    this.removeFromIndex(file.path);
  })
);

Editor-Specific Events

For editor-related functionality, use the CodeMirror state field or editor callbacks:
import { editorInfoField } from 'obsidian';
import { EditorView, ViewUpdate } from '@codemirror/view';

const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
  if (update.docChanged) {
    console.log('Document changed');
  }
  
  if (update.selectionSet) {
    console.log('Selection changed');
  }
});

this.registerEditorExtension(updateListener);

Custom Events

You can create your own event system using the Events class:
import { Events } from 'obsidian';

class MyCustomEvents extends Events {
  // Type-safe event names
  on(name: 'custom-event', callback: (data: string) => any): EventRef;
  on(name: string, callback: (...data: any) => any): EventRef {
    return super.on(name, callback);
  }
}

export default class MyPlugin extends Plugin {
  events: MyCustomEvents;
  
  async onload() {
    this.events = new MyCustomEvents();
    
    // Other plugins can listen
    this.registerEvent(
      this.events.on('custom-event', (data) => {
        console.log('Custom event:', data);
      })
    );
    
    // Trigger the event
    this.events.trigger('custom-event', 'Hello!');
  }
}

Best Practices

Always Register

Always use registerEvent(), registerDomEvent(), and registerInterval() for automatic cleanup.

Filter Early

Filter events as early as possible to avoid unnecessary processing.

Debounce Frequent Events

Use debounce() for events that fire frequently, like modify events.

Type Guards

Use instanceof checks to distinguish between TFile and TFolder.

Complete Example

import { Plugin, TFile, TFolder, debounce } from 'obsidian';

export default class EventExample extends Plugin {
  async onload() {
    // Debounced modify handler
    const debouncedModify = debounce(
      async (file: TFile) => {
        const cache = this.app.metadataCache.getFileCache(file);
        console.log('Headings:', cache?.headings?.length);
      },
      1000,
      true
    );
    
    // Vault events
    this.registerEvent(
      this.app.vault.on('create', (file) => {
        if (file instanceof TFile && file.extension === 'md') {
          console.log('New note created:', file.path);
        }
      })
    );
    
    this.registerEvent(
      this.app.vault.on('modify', (file) => {
        if (file instanceof TFile && file.extension === 'md') {
          debouncedModify(file);
        }
      })
    );
    
    this.registerEvent(
      this.app.vault.on('rename', (file, oldPath) => {
        console.log('Renamed:', oldPath, '->', file.path);
      })
    );
    
    // Workspace events
    this.registerEvent(
      this.app.workspace.on('file-open', (file) => {
        if (file) {
          console.log('Opened:', file.path);
        }
      })
    );
    
    this.registerEvent(
      this.app.workspace.on('layout-change', () => {
        console.log('Layout changed');
      })
    );
    
    // Metadata cache events
    this.registerEvent(
      this.app.metadataCache.on('changed', (file) => {
        console.log('Metadata updated:', file.path);
      })
    );
    
    // DOM events
    this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
      console.log('Click at:', evt.clientX, evt.clientY);
    });
    
    // Interval
    this.registerInterval(
      window.setInterval(() => {
        console.log('Periodic check at:', new Date().toISOString());
      }, 60000) // Every minute
    );
  }
}

Plugin Lifecycle

Learn about lifecycle methods and component registration

App Architecture

Understand the modules that emit events

Build docs developers (and LLMs) love