obsidian-data-handling
Implement vault data backup, sync, and recovery strategies. Use when building backup features, implementing data export, or handling vault synchronization in your plugin. Trigger with phrases like "obsidian backup", "obsidian sync", "obsidian data export", "vault backup strategy".
What this skill does
# Obsidian Data Handling
## Overview
Data management patterns for Obsidian plugins: plugin config with loadData/saveData, vault file I/O, frontmatter parsing via metadataCache, handling renames and deletes, cross-device sync considerations, and IndexedDB fallback for large datasets.
## Prerequisites
- Working Obsidian plugin (`export default class extends Plugin`)
- Understanding of Obsidian's `Vault` and `MetadataCache` APIs
- TypeScript compilation configured
## Instructions
### Step 1: Plugin Config with loadData / saveData
Obsidian stores plugin data in `.obsidian/plugins/<plugin-id>/data.json`. Use `loadData()` and `saveData()` — never read that file directly.
```typescript
interface PluginConfig {
version: number;
apiEndpoint: string;
syncInterval: number;
excludedFolders: string[];
}
const DEFAULT_CONFIG: PluginConfig = {
version: 1,
apiEndpoint: 'https://api.example.com',
syncInterval: 300,
excludedFolders: [],
};
export default class DataPlugin extends Plugin {
config: PluginConfig;
async onload() {
await this.loadConfig();
}
async loadConfig() {
const saved = await this.loadData();
this.config = Object.assign({}, DEFAULT_CONFIG, saved);
// Migrate from older config versions
if (this.config.version < 1) {
this.config.excludedFolders = [];
this.config.version = 1;
await this.saveConfig();
}
}
async saveConfig() {
await this.saveData(this.config);
}
}
```
`loadData()` returns `null` on first run — `Object.assign` onto defaults handles this cleanly.
### Step 2: Reading and Writing Vault Files
```typescript
import { TFile, TFolder, normalizePath } from 'obsidian';
// Read a markdown file
async readNote(path: string): Promise<string | null> {
const file = this.app.vault.getAbstractFileByPath(normalizePath(path));
if (file instanceof TFile) {
return await this.app.vault.read(file);
}
return null;
}
// Write or create a markdown file
async writeNote(path: string, content: string): Promise<TFile> {
const normalized = normalizePath(path);
const existing = this.app.vault.getAbstractFileByPath(normalized);
if (existing instanceof TFile) {
await this.app.vault.modify(existing, content);
return existing;
}
// Ensure parent folder exists
const dir = normalized.substring(0, normalized.lastIndexOf('/'));
if (dir && !this.app.vault.getAbstractFileByPath(dir)) {
await this.app.vault.createFolder(dir);
}
return await this.app.vault.create(normalized, content);
}
// Append to a file (e.g., a log or journal)
async appendToNote(path: string, text: string): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(normalizePath(path));
if (file instanceof TFile) {
await this.app.vault.append(file, '\n' + text);
}
}
```
Use `vault.cachedRead()` instead of `vault.read()` when you don't need the absolute latest content — it avoids hitting disk on every call.
### Step 3: Working with Frontmatter via MetadataCache
Never parse YAML frontmatter manually. Obsidian's `metadataCache` keeps a parsed cache of every file's frontmatter.
```typescript
import { TFile, CachedMetadata } from 'obsidian';
// Read frontmatter from a file
getFrontmatter(file: TFile): Record<string, any> | null {
const cache: CachedMetadata | null = this.app.metadataCache.getFileCache(file);
return cache?.frontmatter ?? null;
}
// Update frontmatter using processFrontMatter (Obsidian 1.4+)
async setStatus(file: TFile, status: string): Promise<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm.status = status;
fm.updated = new Date().toISOString();
});
}
// Bulk query: find all files with a specific tag
getFilesWithTag(tag: string): TFile[] {
const files: TFile[] = [];
for (const file of this.app.vault.getMarkdownFiles()) {
const cache = this.app.metadataCache.getFileCache(file);
const tags = cache?.tags?.map(t => t.tag) ?? [];
const fmTags = cache?.frontmatter?.tags ?? [];
if (tags.includes(tag) || fmTags.includes(tag.replace('#', ''))) {
files.push(file);
}
}
return files;
}
```
`processFrontMatter` handles YAML serialization correctly — it preserves comments and formatting, and is the only safe way to update frontmatter programmatically.
### Step 4: Handling File Renames and Deletes
Plugins that index file paths must update their state when files move or disappear.
```typescript
async onload() {
// Track renames to update internal references
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
if (file instanceof TFile) {
this.onFileRenamed(file, oldPath);
}
})
);
// Clean up when files are deleted
this.registerEvent(
this.app.vault.on('delete', (file) => {
if (file instanceof TFile) {
this.onFileDeleted(file.path);
}
})
);
}
private onFileRenamed(file: TFile, oldPath: string) {
// Update any stored path references
if (this.config.pinnedFiles?.includes(oldPath)) {
const idx = this.config.pinnedFiles.indexOf(oldPath);
this.config.pinnedFiles[idx] = file.path;
this.saveConfig();
}
}
private onFileDeleted(path: string) {
// Remove from any indexes
if (this.config.pinnedFiles?.includes(path)) {
this.config.pinnedFiles = this.config.pinnedFiles.filter(p => p !== path);
this.saveConfig();
}
}
```
Always use `registerEvent` — it automatically cleans up the listener when the plugin unloads.
### Step 5: Cross-Device Sync Considerations
Obsidian vaults synced via iCloud, Dropbox, or Obsidian Sync introduce eventual consistency issues.
```typescript
// Problem: two devices modify data.json simultaneously
// Solution: merge-friendly data structures
interface SyncSafeConfig {
// Use a map keyed by unique IDs instead of arrays
// Maps merge better than arrays across sync conflicts
items: Record<string, { value: string; updatedAt: number }>;
}
// Timestamp-based last-write-wins merge
mergeConfigs(local: SyncSafeConfig, remote: SyncSafeConfig): SyncSafeConfig {
const merged: SyncSafeConfig = { items: {} };
const allKeys = new Set([
...Object.keys(local.items),
...Object.keys(remote.items),
]);
for (const key of allKeys) {
const l = local.items[key];
const r = remote.items[key];
if (!l) merged.items[key] = r;
else if (!r) merged.items[key] = l;
else merged.items[key] = l.updatedAt >= r.updatedAt ? l : r;
}
return merged;
}
```
Guidelines for sync-friendly plugins:
- Avoid storing file paths in `data.json` — they differ across devices with different vault locations
- Use file content hashes or frontmatter IDs for identity instead of paths
- Keep `data.json` small — large files cause sync conflicts and slow sync
### Step 6: IndexedDB Fallback for Large Datasets
When plugin data exceeds what's practical for `data.json` (more than ~1MB), use IndexedDB.
```typescript
class PluginDatabase {
private db: IDBDatabase | null = null;
private dbName: string;
constructor(pluginId: string) {
this.dbName = `obsidian-${pluginId}`;
}
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('cache')) {
db.createObjectStore('cache', { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async put(id: string, data: any): Promise<void> {
if (!this.db) throw new Error('Database not open');
return new Promise((resolve, reject) => {
const tx = this.db!.transaction('cache', 'readwrite');
tx.objectStore('cache').put({ id, data, updatedAt: Date.now() });
tx.oncomplete = () => resolveRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.