obsidian-core-workflow-b

Build advanced Obsidian plugin features: custom views (ItemView), modals, editor commands with selection manipulation, status bar, context menus, and Vault API file creation/modification. Use when adding UI components to a plugin, building sidebar views, or creating modal dialogs. Trigger with "obsidian modal", "obsidian custom view", "obsidian sidebar", "obsidian context menu", "obsidian editor command", "obsidian UI".

1,868 stars

Best use case

obsidian-core-workflow-b is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build advanced Obsidian plugin features: custom views (ItemView), modals, editor commands with selection manipulation, status bar, context menus, and Vault API file creation/modification. Use when adding UI components to a plugin, building sidebar views, or creating modal dialogs. Trigger with "obsidian modal", "obsidian custom view", "obsidian sidebar", "obsidian context menu", "obsidian editor command", "obsidian UI".

Teams using obsidian-core-workflow-b should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/obsidian-core-workflow-b/SKILL.md --create-dirs "https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/main/plugins/saas-packs/obsidian-pack/skills/obsidian-core-workflow-b/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/obsidian-core-workflow-b/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How obsidian-core-workflow-b Compares

Feature / Agentobsidian-core-workflow-bStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build advanced Obsidian plugin features: custom views (ItemView), modals, editor commands with selection manipulation, status bar, context menus, and Vault API file creation/modification. Use when adding UI components to a plugin, building sidebar views, or creating modal dialogs. Trigger with "obsidian modal", "obsidian custom view", "obsidian sidebar", "obsidian context menu", "obsidian editor command", "obsidian UI".

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Obsidian Core Workflow B: Advanced Plugin Features

## Overview

Add production UI to an existing Obsidian plugin: custom sidebar views,
modal dialogs with forms, fuzzy-search suggestion popups, editor commands
that manipulate selections, status bar widgets, context menus, and
programmatic file creation via the Vault API. Every snippet is a complete,
copy-pasteable class.

## Prerequisites

- A working plugin built with `obsidian-core-workflow-a` (or equivalent)
- `npm install --save-dev obsidian` already done
- Familiarity with the Plugin lifecycle (`onload` / `onunload`)

## Instructions

### Step 1: Custom sidebar view (ItemView)

A custom view registers a new panel type that can live in the left or right sidebar.

```typescript
// src/views/StatsView.ts
import { ItemView, WorkspaceLeaf, TFile } from "obsidian";

export const STATS_VIEW_TYPE = "vault-stats-view";

export class StatsView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType(): string {
    return STATS_VIEW_TYPE;
  }

  getDisplayText(): string {
    return "Vault Stats";
  }

  getIcon(): string {
    return "bar-chart-2";
  }

  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.addClass("stats-view");

    container.createEl("h4", { text: "Vault Statistics" });
    const listEl = container.createEl("ul");

    const files = this.app.vault.getMarkdownFiles();
    let totalWords = 0;

    for (const file of files) {
      const content = await this.app.vault.cachedRead(file);
      totalWords += content.split(/\s+/).filter(Boolean).length;
    }

    listEl.createEl("li", { text: `Notes: ${files.length}` });
    listEl.createEl("li", { text: `Total words: ${totalWords.toLocaleString()}` });
    listEl.createEl("li", {
      text: `Avg words/note: ${files.length ? Math.round(totalWords / files.length) : 0}`,
    });

    // Refresh button
    const btn = container.createEl("button", { text: "Refresh" });
    btn.addEventListener("click", () => this.onOpen());
  }

  async onClose() {
    // cleanup if needed
  }
}
```

Register and open it from your main plugin:

```typescript
// In your Plugin's onload():
import { StatsView, STATS_VIEW_TYPE } from "./views/StatsView";

this.registerView(STATS_VIEW_TYPE, (leaf) => new StatsView(leaf));

this.addCommand({
  id: "open-stats-view",
  name: "Open vault stats",
  callback: () => this.activateStatsView(),
});

this.addRibbonIcon("bar-chart-2", "Vault Stats", () => this.activateStatsView());

// Helper to open or reveal the view
async activateStatsView() {
  const { workspace } = this.app;
  let leaf = workspace.getLeavesOfType(STATS_VIEW_TYPE)[0];

  if (!leaf) {
    const rightLeaf = workspace.getRightLeaf(false);
    if (rightLeaf) {
      await rightLeaf.setViewState({ type: STATS_VIEW_TYPE, active: true });
      leaf = rightLeaf;
    }
  }
  if (leaf) workspace.revealLeaf(leaf);
}

// In onunload():
this.app.workspace.detachLeavesOfType(STATS_VIEW_TYPE);
```

### Step 2: Modal dialogs

**Confirmation modal** -- returns a boolean via callback:

```typescript
// src/modals/ConfirmModal.ts
import { App, Modal, Setting } from "obsidian";

export class ConfirmModal extends Modal {
  private resolved = false;
  constructor(
    app: App,
    private message: string,
    private onResult: (confirmed: boolean) => void
  ) {
    super(app);
  }

  onOpen() {
    const { contentEl } = this;
    contentEl.createEl("h3", { text: "Confirm" });
    contentEl.createEl("p", { text: this.message });

    new Setting(contentEl)
      .addButton((btn) =>
        btn.setButtonText("Cancel").onClick(() => this.close())
      )
      .addButton((btn) =>
        btn
          .setButtonText("Confirm")
          .setCta()
          .onClick(() => {
            this.resolved = true;
            this.close();
          })
      );
  }

  onClose() {
    this.onResult(this.resolved);
    this.contentEl.empty();
  }
}
```

**Text input modal** -- collects a single string:

```typescript
// src/modals/InputModal.ts
import { App, Modal, Setting } from "obsidian";

export class InputModal extends Modal {
  private value = "";
  constructor(
    app: App,
    private title: string,
    private placeholder: string,
    private onSubmit: (value: string | null) => void
  ) {
    super(app);
  }

  onOpen() {
    const { contentEl } = this;
    contentEl.createEl("h3", { text: this.title });

    new Setting(contentEl).addText((text) =>
      text
        .setPlaceholder(this.placeholder)
        .onChange((v) => (this.value = v))
    );

    new Setting(contentEl)
      .addButton((btn) =>
        btn.setButtonText("Cancel").onClick(() => {
          this.onSubmit(null);
          this.close();
        })
      )
      .addButton((btn) =>
        btn
          .setButtonText("OK")
          .setCta()
          .onClick(() => {
            this.onSubmit(this.value);
            this.close();
          })
      );
  }

  onClose() {
    this.contentEl.empty();
  }
}
```

### Step 3: Fuzzy suggestion modal (SuggestModal)

Opens a searchable list. Users type to filter, then pick an item.

```typescript
// src/modals/NotePicker.ts
import { App, FuzzySuggestModal, TFile } from "obsidian";

export class NotePicker extends FuzzySuggestModal<TFile> {
  constructor(app: App, private onPick: (file: TFile) => void) {
    super(app);
  }

  getItems(): TFile[] {
    return this.app.vault.getMarkdownFiles();
  }

  getItemText(file: TFile): string {
    return file.path;
  }

  onChooseItem(file: TFile): void {
    this.onPick(file);
  }
}

// Usage in a command:
this.addCommand({
  id: "pick-note",
  name: "Pick a note",
  callback: () => {
    new NotePicker(this.app, (file) => {
      new Notice(`Selected: ${file.basename}`);
    }).open();
  },
});
```

### Step 4: Editor commands with selection manipulation

`editorCallback` gives you the CodeMirror `Editor` and the active `MarkdownView`.

```typescript
// Wrap selection in callout
this.addCommand({
  id: "wrap-callout",
  name: "Wrap selection in callout",
  editorCallback: (editor, view) => {
    const selection = editor.getSelection();
    if (!selection) {
      new Notice("Select text first");
      return;
    }
    const callout = `> [!note]\n> ${selection.split("\n").join("\n> ")}`;
    editor.replaceSelection(callout);
  },
});

// Insert ISO timestamp at cursor
this.addCommand({
  id: "insert-timestamp",
  name: "Insert timestamp",
  editorCallback: (editor) => {
    const now = new Date().toISOString().slice(0, 19).replace("T", " ");
    editor.replaceSelection(now);
  },
});

// Sort selected lines alphabetically
this.addCommand({
  id: "sort-lines",
  name: "Sort selected lines",
  editorCallback: (editor) => {
    const selection = editor.getSelection();
    if (!selection) return;
    const sorted = selection.split("\n").sort((a, b) => a.localeCompare(b)).join("\n");
    editor.replaceSelection(sorted);
  },
});
```

### Step 5: Status bar items

Status bar items sit at the bottom of the Obsidian window.

```typescript
// In onload():
const statusEl = this.addStatusBarItem();
statusEl.setText("Words: --");

// Update word count when active file changes
this.registerEvent(
  this.app.workspace.on("active-leaf-change", async () => {
    const view = this.app.workspace.getActiveViewOfType(MarkdownView);
    if (view) {
      const content = view.editor.getValue();
      const count = content.split(/\s+/).filter(Boolean).length;
      statusEl.setText(`Words: ${count}`);
    } else {
      statusEl.setText("Words: --");
    }
  })
);
```

### Step 6: Context menus

Add items to the file explorer right-click menu and the editor right-click menu.

```typescript
// File explorer context menu -- only on markdown files
this.registerEvent(
  this.app.workspace.on("file-menu", (menu, file) => {
    if (file instanceof TFile && file.extension === "md") {
      menu.addItem((item) => {
        item
          .setTitle("Copy note title")
          .setIcon("clipboard-copy")
          .onClick(async () => {
            await navigator.clipboard.writeText(file.basename);
            new Notice(`Copied: ${file.basename}`);
          });
      });
    }
  })
);

// Editor context menu -- insert current date
this.registerEvent(
  this.app.workspace.on("editor-menu", (menu, editor) => {
    menu.addItem((item) => {
      item
        .setTitle("Insert today's date")
        .setIcon("calendar")
        .onClick(() => {
          const today = new Date().toISOString().slice(0, 10);
          editor.replaceSelection(today);
        });
    });
  })
);
```

### Step 7: File creation and modification via Vault API

Programmatically create, read, and modify notes.

```typescript
// Create a daily note if it doesn't exist
async ensureDailyNote(): Promise<TFile> {
  const today = new Date().toISOString().slice(0, 10);
  const path = `Daily/${today}.md`;

  const existing = this.app.vault.getAbstractFileByPath(path);
  if (existing instanceof TFile) return existing;

  // Ensure folder
  const folder = this.app.vault.getAbstractFileByPath("Daily");
  if (!folder) await this.app.vault.createFolder("Daily");

  const content = `# ${today}\n\n## Tasks\n\n- [ ] \n\n## Notes\n\n`;
  return this.app.vault.create(path, content);
}

// Append text to the end of a note
async appendToNote(file: TFile, text: string): Promise<void> {
  const current = await this.app.vault.read(file);
  await this.app.vault.modify(file, current + "\n" + text);
}

// Batch-update frontmatter tag across files
async addTagToFolder(folder: string, tag: string): Promise<number> {
  const files = this.app.vault.getMarkdownFiles()
    .filter((f) => f.path.startsWith(folder + "/"));

  let count = 0;
  for (const file of files) {
    let content = await this.app.vault.read(file);
    if (content.startsWith("---")) {
      // Has frontmatter -- insert tag
      content = content.replace(
        /^(---\n[\s\S]*?)(---)$/m,
        `$1tags:\n  - ${tag}\n$2`
      );
    } else {
      // No frontmatter -- add it
      content = `---\ntags:\n  - ${tag}\n---\n${content}`;
    }
    await this.app.vault.modify(file, content);
    count++;
  }
  return count;
}
```

## Output

After applying these patterns your plugin gains:
- A sidebar panel (ItemView) with live vault statistics
- Confirmation and text-input modal dialogs
- A fuzzy-search note picker
- Editor commands that transform selected text
- A live word-count status bar widget
- Right-click context menu extensions
- Vault API helpers for file creation and batch modification

## Error Handling

| Error | Cause | Fix |
|-------|-------|-----|
| View not appearing | Forgot `registerView` in `onload` | Must register before opening |
| `getRightLeaf` returns null | No sidebar available | Guard with `if (leaf)` |
| Modal closes without callback | Event order issue | Set result before calling `this.close()` |
| `editorCallback` greyed out | No active markdown editor | Use `callback` instead for non-editor commands |
| Context menu item missing | Wrong event name | `file-menu` for explorer, `editor-menu` for editor |
| `vault.create` throws | File already exists at path | Check with `getAbstractFileByPath` first |
| Stale `cachedRead` data | Cache not yet updated | Use `vault.read` when freshness matters |

## Examples

**Open a view in a new tab instead of sidebar:**
```typescript
const leaf = this.app.workspace.getLeaf("tab");
await leaf.setViewState({ type: STATS_VIEW_TYPE, active: true });
```

**Promise-based confirm modal:**
```typescript
function confirm(app: App, msg: string): Promise<boolean> {
  return new Promise((resolve) => {
    new ConfirmModal(app, msg, resolve).open();
  });
}

// Usage:
if (await confirm(this.app, "Delete all empty notes?")) {
  // proceed
}
```

## Resources

- [ItemView API](https://docs.obsidian.md/Reference/TypeScript+API/ItemView)
- [Modal API](https://docs.obsidian.md/Reference/TypeScript+API/Modal)
- [FuzzySuggestModal](https://docs.obsidian.md/Reference/TypeScript+API/FuzzySuggestModal)
- [Editor API](https://docs.obsidian.md/Reference/TypeScript+API/Editor)
- [Vault API](https://docs.obsidian.md/Reference/TypeScript+API/Vault)
- [Lucide Icons](https://lucide.dev/icons/)

## Next Steps

- Set up hot-reload development: see `obsidian-local-dev-loop`
- Apply production safety patterns: see `obsidian-sdk-patterns`
- Handle common errors: see `obsidian-common-errors`

Related Skills

calendar-to-workflow

1868
from jeremylongshore/claude-code-plugins-plus-skills

Converts calendar events and schedules into Claude Code workflows, meeting prep documents, and standup notes. Use when the user mentions calendar events, meeting prep, standup generation, or scheduling workflows. Trigger with phrases like "prep for my meetings", "generate standup notes", "create workflow from calendar", or "summarize today's schedule".

workhuman-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Workhuman core workflow b for employee recognition and rewards API. Use when integrating Workhuman Social Recognition, or building recognition workflows with HRIS systems. Trigger: "workhuman core workflow b".

workhuman-core-workflow-a

1868
from jeremylongshore/claude-code-plugins-plus-skills

Workhuman core workflow a for employee recognition and rewards API. Use when integrating Workhuman Social Recognition, or building recognition workflows with HRIS systems. Trigger: "workhuman core workflow a".

wispr-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Wispr Flow core workflow b for voice-to-text API integration. Use when integrating Wispr Flow dictation, WebSocket streaming, or building voice-powered applications. Trigger: "wispr core workflow b".

wispr-core-workflow-a

1868
from jeremylongshore/claude-code-plugins-plus-skills

Wispr Flow core workflow a for voice-to-text API integration. Use when integrating Wispr Flow dictation, WebSocket streaming, or building voice-powered applications. Trigger: "wispr core workflow a".

windsurf-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Execute Windsurf's secondary workflow: Workflows, Memories, and reusable automation. Use when creating reusable Cascade workflows, managing persistent memories, or automating repetitive development tasks. Trigger with phrases like "windsurf workflow", "windsurf automation", "windsurf memories", "cascade workflow", "windsurf slash command".

windsurf-core-workflow-a

1868
from jeremylongshore/claude-code-plugins-plus-skills

Execute Windsurf's primary workflow: Cascade Write mode for multi-file agentic coding. Use when building features, refactoring across files, or performing complex code tasks. Trigger with phrases like "windsurf cascade write", "windsurf agentic coding", "windsurf multi-file edit", "cascade write mode", "windsurf build feature".

webflow-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Execute Webflow secondary workflows — Sites management, Pages API, Forms submissions, Ecommerce (products/orders/inventory), and Custom Code via the Data API v2. Use when managing sites, reading pages, handling form data, or working with Webflow Ecommerce products and orders. Trigger with phrases like "webflow sites", "webflow pages", "webflow forms", "webflow ecommerce", "webflow products", "webflow orders".

webflow-core-workflow-a

1868
from jeremylongshore/claude-code-plugins-plus-skills

Execute the primary Webflow workflow — CMS content management: list collections, CRUD items, publish items, and manage content lifecycle via the Data API v2. Use when working with Webflow CMS collections and items, managing blog posts, team members, or any dynamic content. Trigger with phrases like "webflow CMS", "webflow collections", "webflow items", "create webflow content", "manage webflow CMS", "webflow content management".

veeva-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Veeva Vault core workflow b for REST API and clinical operations. Use when working with Veeva Vault document management and CRM. Trigger: "veeva core workflow b".

veeva-core-workflow-a

1868
from jeremylongshore/claude-code-plugins-plus-skills

Veeva Vault core workflow a for REST API and clinical operations. Use when working with Veeva Vault document management and CRM. Trigger: "veeva core workflow a".

vastai-core-workflow-b

1868
from jeremylongshore/claude-code-plugins-plus-skills

Execute Vast.ai secondary workflow: multi-instance orchestration, spot recovery, and cost optimization. Use when running distributed training, handling spot preemption, or optimizing GPU spend across multiple instances. Trigger with phrases like "vastai distributed training", "vastai spot recovery", "vastai multi-gpu", "vastai cost optimization".