agentsclimarketplace

Obsidian meta bind

Skill lexbritvin/obsidian-skills-pack/skills/obsidian-meta-bind

Agent Skills for Obsidian — plugin syntax (queries, tasks, charts, interactive UI), graph view tuning, vault auditing, heatmap visualizations. Compatible with Claude Code, Codex CLI, OpenCode, and any agent-skills runtime.

Install
npx -y skills add lexbritvin/obsidian-skills-pack --skill obsidian-meta-bind

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Authoring buttons, input fields, view fields, and embeds for the Obsidian Meta Bind plugin. Use whenever the user asks to add an interactive control, button, input, viewfield, INPUT[...] / VIEW[...] / BUTTON[...] syntax, or a meta-bind code block. Covers schema, valid enum values, inline references, JS Engine integration, and the most common validation pitfalls (e.g. "Invalid input at 'style'", "Button ID not found", trailing-comma traps).

SKILL.md

31.9 KB, as published. Nobody here has run it

Meta Bind — buttons, inputs, view fields

This skill covers the Meta Bind Obsidian plugin (id obsidian-meta-bind-plugin, by Moritz Jung). Authoring rules, full schemas, JS Engine integration, and the validation traps that cost hours when not known up front.

Docs root: https://www.moritzjung.dev/obsidian-meta-bind-plugin-docs/.

When to use this skill

Reach for this skill on any of:

  • User mentions Meta Bind, INPUT[...], VIEW[...], BUTTON[...], ```meta-bind-button, ```meta-bind code blocks.
  • User wants to add an interactive widget into a note (toggle, slider, button, dropdown, date picker, etc.).
  • User reports META_BIND_ERROR, [MB_BUTTON], Button ID not Found, "validation failed", "Invalid input at 'style'", or any Meta Bind error overlay.
  • User wants to bind metadata across files (file^property, memory^..., globalMemory^...).
  • User needs to wire a button to an Obsidian command, JS code, file open, metadata mutation, or templater.
  • User authors a tracker or dashboard with declarative INPUT fields tied to frontmatter.

Prerequisites

Confirm before debugging:

  1. Meta Bind plugin installed and enabled. Check .obsidian/plugins/obsidian-meta-bind-plugin/manifest.json.
  2. JS Engine plugin installed and enabled if any button uses action.type: inlineJS or js. Without it, JS actions silently fail.
  3. Settings flag enableJs: true in .obsidian/plugins/obsidian-meta-bind-plugin/data.json. JS-based buttons and JS view fields don't run otherwise.
  4. excludedFolders in the same data.json: any path matching an entry is skipped by Meta Bind. Default contains templates. If a tracker file lives under Templates/, Meta Bind ignores it. Move the file or remove the exclusion.

Button schema — every field

Source: https://www.moritzjung.dev/obsidian-meta-bind-plugin-docs/guides/buttons/#button-configuration.

FieldTypeRequiredNotes
labelstringyesDisplay text on the button.
styleenumyesOne of default, primary, destructive, plain. Validation fails if missing or out of enum — error reads Invalid input at 'style'.
iconstringnoLucide icon name.
classstringnoSpace-separated CSS classes.
cssStylestringnoInline style rules (CSS text).
backgroundImagestringnoURL or vault path.
tooltipstringnoHover text. Defaults to label when unset.
idstringnoUsed for BUTTON[id] inline references in the same note.
hiddenbooleannoWhen true the code-block declaration is not rendered itself; only BUTTON[id] references render the button.
actionobjectno*Single action; mutually exclusive with actions.
actionsobject[]no*Array of actions executed in sequence; mutually exclusive with action.

*One of action or actions is required for the button to do anything.

Minimal working button

label: Run Script
style: primary
action:
  type: inlineJS
  code: console.log('clicked')

Inline reference + hidden declaration

Click here: `BUTTON[my-button-id]`

```meta-bind-button
label: Do thing
id: my-button-id
style: primary
hidden: true
action:
  type: command
  command: app:reload

The declaration is hidden; the inline `BUTTON[my-button-id]` renders the actual button at the cursor position. If `BUTTON[x]` shows `Button ID not Found`, the declaration with `id: x` either doesn't exist in the same file or **failed validation** and was dropped from the index — open the error overlay to see why.

## Button actions — types and schemas

Source: <https://www.moritzjung.dev/obsidian-meta-bind-plugin-docs/reference/buttonactions/...>. Action types confirmed in the plugin source: `command`, `open`, `input`, `sleep`, `inlineJS`, `js`, `templaterCreateNote`, plus metadata actions.

### `command` — run an Obsidian command

```yaml
action:
  type: command
  command: obsidian-meta-bind-plugin:open-faq

Find command IDs via the Meta Bind helper command "Select and copy command id".

open — open a file or URL

action:
  type: open
  link: "[[Some Note]]"     # or "https://example.com"
  newTab: false             # optional

inlineJS — run JS code embedded in YAML

Requires JS Engine plugin + enableJs: true in Meta Bind settings.

action:
  type: inlineJS
  code: |
    const file = app.workspace.getActiveFile();
    new Notice("hi from " + file.basename);

Inside the code, two extras are available: context.buttonConfig (the button's own config, read-only) and context.buttonContext (button metadata). Standard Obsidian globals — app, Notice, window.moment, etc. — all work.

js — run a JS file from the vault

action:
  type: js
  file: scripts/myAction.js

The JS file lives in the vault. Same JS Engine + enableJs requirement.

sleep — pause between actions (only useful with actions:)

action:
  type: sleep
  ms: 500

input — type text at the cursor (in the editor)

action:
  type: input
  str: "some text"

insertIntoNote — insert text or Templater output at a line

action:
  type: insertIntoNote
  line: "selfEnd + 1"     # absolute number or relative expression
  value: "Hello, world!"  # plain text — or a Templater template path
  templater: false        # set true if value is a template path

Relative-line expressions: selfStart, selfEnd, selfStart - 1, selfEnd + 1 (relative to the button's own position). Absolute number also accepted.

replaceInNote — replace a line range

action:
  type: replaceInNote
  fromLine: 3
  toLine: 5
  replacement: "## New section\n\nfresh content"
  templater: false        # true → replacement is a template path

templaterCreateNote — new note from a Templater template

action:
  type: templaterCreateNote
  templateFile: "Templates/Lecture.md"
  folderPath: "Lectures"          # optional, default = vault root
  fileName: "New Lecture - RENAME ME"  # optional
  openNote: true                  # optional
  openIfAlreadyExists: false      # optional — opens existing instead of duplicate

createNote — new empty note

action:
  type: createNote
  fileName: "New Note"
  folderPath: "My Folder"     # optional
  openNote: false             # optional
  openIfAlreadyExists: false  # optional — appends running number on conflict

Other action types (consult docs when needed)

  • setMetadata / updateMetadata — write to frontmatter at a bind target.
  • regexp — regex search-and-replace within the host note.
  • runTemplaterFile — execute a Templater template as a script (no new note).

Reference URL pattern: /reference/buttonactions/<typeName>/.

Multiple actions in one button

actions:
  - type: command
    command: theme:use-light
  - type: sleep
    ms: 1000
  - type: command
    command: theme:use-dark

Input fields — schema

Inline syntax: `INPUT[type(args):bindTarget]`. Arguments go inside type(...). The bind target after : is optional but strongly recommended — without it, the input loses state on note reload.

Full type list:

TypeInline formCommon arguments
toggleINPUT[toggle:done]onValue(), offValue()
sliderINPUT[slider(minValue(0), maxValue(10)):rating]minValue(), maxValue(), stepSize()
textINPUT[text(placeholder('name')):name]placeholder(), defaultValue()
textAreaINPUT[textArea:notes]placeholder()
numberINPUT[number(minValue(0)):count]minValue(), maxValue()
dateINPUT[date:due]
timeINPUT[time:meet]
datePickerINPUT[datePicker:deadline]
selectINPUT[select(option(a), option(b)):choice]option()
multiSelectINPUT[multiSelect(option(x), option(y)):tags]option()
inlineSelectINPUT[inlineSelect(option(yes), option(no)):flag]option()
suggesterINPUT[suggester(option(A), option(B)):val]option()
editorINPUT[editor:body]
progressBarINPUT[progressBar(minValue(0), maxValue(100)):pct]minValue(), maxValue()
listINPUT[list:items]
listSuggesterINPUT[listSuggester(option(x)):items]option()
imageSuggesterINPUT[imageSuggester:img]
inlineListINPUT[inlineList:keywords]
inlineListSuggesterINPUT[inlineListSuggester(option(t)):tags]option()

option() two-arg form option(value, displayName) distinguishes stored value from rendered label.

To author a more complex input field, use a fenced code block:

```meta-bind
INPUT[select(
  option(low, "Low priority"),
  option(med, "Medium"),
  option(high, "High priority"),
  class(my-css-class)
):priority]
```

Input field arguments — full list

Arguments go inside INPUT[type(arg1, arg2, ...):bindTarget]. Multiple comma-separated.

ArgumentApplies toFormExample
option(value) / option(value, displayName)select, multiSelect, suggester, listSuggester, inlineSelect, inlineListSuggester, imageSuggesterTwo-form: with one arg, value = displayName. With two args, first = stored value, second = visible label. Type-coerced: true/false → bool, null → null, numeric → number.option(1, "1 Star")
optionQuery(query)suggester familyDataview-like query that resolves to a list of options dynamically.optionQuery("#tag")
defaultValue(value)text, textArea, number, date, timePre-fills when bind target is empty.defaultValue("Untitled")
placeholder(text)text, textArea, numberGreyed-out hint inside an empty input.placeholder("Enter name")
minValue(n), maxValue(n), stepSize(n)slider, number, progressBarNumeric bounds and step.minValue(0), maxValue(10), stepSize(0.5)
addLabelssliderShow numeric labels alongside the slider.addLabels
allowOthersuggester familyAccept user input outside the option list.allowOther
class(className)all input field typesAdd CSS class for custom styling. Multiple supported.class(my-css-class)
showcaseallDisplay the bind target name + value beside the input — useful for debugging.showcase
title(text)allOverride the visible label / accessibility title.title("Priority")
useLinks(true|false)suggester variantsWhether selected values are stored as [[wikilinks]].useLinks(true)
onValue(v), offValue(v)toggleOverride what gets written for true/false (default: true/false booleans).onValue("done"), offValue("todo")

Class names with special chars must be quoted: class("group-1").

For value strings with special chars (commas, parens), use single or double quotes inside the argument: option("yes, please", "Yes").

View fields — schema

Inline syntax: `VIEW[expression][type]`. Default type is math; can be omitted for math expressions.

Types: math, text, image, link, plus jsViewField for arbitrary JS-driven views.

Property references inside expression use {propertyName} and follow the same bind target rules as inputs.

`VIEW[round(number({distance} km, miles), 2)]`           <!-- math (default) -->
`VIEW[**{title}**][text(renderMarkdown)]`                <!-- bold via text type -->
`VIEW[{cover}][image]`                                   <!-- image from frontmatter -->
`VIEW[{relatedNote}][link]`                              <!-- clickable link -->

JS View Fields

jsViewField is a code block that lets arbitrary JS render a value, with reactive bind targets. It needs JS Engine + enableJs: true.

bindTargets:
  distance: this#distanceKm
hidden: true
---
return Math.round(distance * 0.621371);

The runner injects each bindTarget as a top-level JS variable. The function returns the rendered value (string / number / DOM Node). Updates re-run when any binding changes.

Use cases: charts, computed labels, lookups in a contact list, anything beyond the math/text view fields.

Meta Bind Embeds

A way to embed a note's interactive widgets as if they belonged to the host file. Standard ![[Note]] keeps widgets bound to the source's frontmatter; meta-bind embeds re-bind to the container note.

Inline:

`META_BIND_EMBED[[Note A]]`

Block:

```meta-bind-embed[[Note A]]```

Use case: a "tracker template note" with INPUT/VIEW/BUTTON declarations, embedded into many host notes. Each host note gets its own bound state.

Caveat: deep nesting (embed-inside-embed) is limited; check current docs version.

Settings (data.json) — the full set

KeyDefaultEffect
devModefalseDev-mode logs in console.
ignoreCodeBlockRestrictionsfalseIf true, Meta Bind also processes plain code blocks, not just its own. Risky.
preferredDateFormatYYYY-MM-DDUsed by date inputs / view fields.
firstWeekday{index:1,name:"Monday",...}Calendar pickers.
syncInterval200ms between metadata sync passes.
enableJsfalseToggles inlineJS / js actions and jsViewField. Required for any JS-driven element.
viewFieldDisplayNullAsEmptyfalseIf true, missing values render as empty string instead of null.
enableSyntaxHighlightingtrueYAML highlighting in meta-bind code blocks.
enableEditorRightClickMenutrueRight-click → "Insert input field/button".
inputFieldTemplates[]Reusable INPUT-field templates. Format: { id, declaration }. Reference via INPUT[id][overrides].
buttonTemplates[]Reusable button templates. Same fields as a button declaration, plus id. Reference via `BUTTON[id]`.
excludedFolders["templates"]Path-substring matches; Meta Bind skips files under those folders entirely.

ObsAPI — public methods

const mb = app.plugins.getPlugin("obsidian-meta-bind-plugin").api;

MethodPurpose
createField(filePath, opts)Generic field creator (input/view/jsView).
createInputFieldMountable(filePath, opts)Build a mountable input field programmatically.
createViewFieldMountable(filePath, opts)Build a mountable view field.
createJsViewFieldMountable(filePath, opts)Build a mountable JS view field.
createTableMountable(filePath, opts)Build a tracker/table mountable.
createButtonMountable(filePath, opts)Build a mountable button (use this when rendering programmatic buttons from another plugin).
createButtonGroupMountable(filePath, opts)Group of buttons.
createEmbedMountable(filePath, opts)Programmatic meta-bind-embed.
createExcludedMountable(filePath)The "excluded folder" placeholder UI.
createInlineFieldFromString(str, ...)Parse a INPUT[...] / VIEW[...] / BUTTON[...] string.
createInlineFieldOfTypeFromString(type, str, ...)Same but with explicit kind.
wrapInMDRC(mountable, container, ctx)Lifecycle helper — handles mount/unmount via Obsidian's MarkdownRenderChild. Always use this when mounting a Meta Bind component manually.
constructMDRCWidget(mountable, ...)Lower-level CodeMirror 6 widget constructor (live-preview path).
createBindTarget(storageType, filePath, propertyPath)Programmatic bind target.
parseBindTarget(str)Parse a bind-target string.
createNotePosition(...)Position reference for insertIntoNote etc.
createSignal(initial)Reactive signal — fire-and-subscribe.
getMetadata(bindTarget)Read frontmatter / memory at a bind target.
setMetadata(bindTarget, value)Write.
updateMetadata(bindTarget, fn)Read-modify-write.
subscribeToMetadata(bindTarget, fn)React to changes.
reactiveMetadata(bindTarget)Wrap a bind target in a Signal for reactive UIs.

There is no public setButtonTemplate / registerButtonTemplate. To populate templates programmatically, you must reach into mb.api.mb.buttonManager.buttonTemplates (internal). See "zero data.json" pattern above.

Bind targets — full syntax

storageType^storagePath#property. Both storageType^ and storagePath# are optional.

Storage typeNotes
frontmatter (default)YAML at the top of a file.
memoryPer-file ephemeral; lost when no file references it for a while or on Obsidian restart.
globalMemoryVault-wide ephemeral, non-persisted.
scopeExtends another bind target without explicit storage path.

Examples:

INPUT[toggle:completed]                    # local frontmatter
INPUT[toggle:Task A#completed]             # frontmatter in another file
INPUT[toggle:path/to/Task A#completed]     # frontmatter in subfolder
INPUT[text:memory^Task A#draft]            # ephemeral memory keyed to a file
INPUT[text:globalMemory^#counter]          # vault-wide ephemeral
INPUT[toggle:this.is.nested]               # nested via dot
INPUT[toggle:["with spaces"]]              # bracketed for special chars

Common validation traps (the painful ones)

  1. style is required. Missing or out-of-enum value triggers Invalid input at 'style'. Always set one of default | primary | destructive | plain.

  2. JS Engine plugin missing. Buttons with inlineJS / js action will fail to run (often silently — check developer console). Install JS Engine and toggle enableJs: true in Meta Bind settings.

  3. excludedFolders in data.json (default ["templates"]) makes Meta Bind skip any file under those folders entirely — buttons there won't render. Match is path-substring against the lowercased file path.

  4. Trailing commas in inlineJS code blocks. Object/array literals with trailing commas can fail validation in Meta Bind 1.4.x — strip them. Convert

    tk.fn({ a: 1, b: 2, })
    

    to

    tk.fn({ a: 1, b: 2 })
    
  5. Multi-line object literals in inlineJS. Less reliable than inline form — when in doubt, write the call on a single line, or hoist the object into a variable on its own line:

    action:
      type: inlineJS
      code: |
        const opts = { foo: "x", bar: "y" };
        await someFn(opts);
    
  6. Button ID not Found on BUTTON[id] inline reference. Causes:

    • declaration block has a style / schema validation error — it was dropped from the index;
    • id: in the declaration doesn't match the inline reference;
    • declaration is in a different file (Meta Bind only resolves IDs in the same note);
    • file lives under an excludedFolders entry;
    • template registered programmatically, but rendering ran first — see trap #12 (race on cold start).
  7. YAML quoting around emojis and Unicode. Emojis like 📂 in label: are fine when wrapped in "...". Bare unquoted emoji at the start of a value can confuse the YAML parser; quote when in doubt.

  8. hidden: true is not a substitute for action. Hidden declarations still need a valid action or actions — they're rendered through the inline reference, not skipped.

  9. enableJs: true flips a permission flag but doesn't re-load currently rendered notes — toggling the flag, or reloading Obsidian (Cmd+P → Reload app without saving), is sometimes needed.

  10. Inline INPUT/VIEW in callout/blockquote. Inline fields render normally, but watch for Markdown reader confusion when nested inside callouts — fenced meta-bind code blocks are safer for complex cases.

  11. button with id "X" already exists in the button templates. Triggered when a meta-bind-button declaration in a file shares its id with a template already in buttonManager.buttonTemplates (whether from data.json or programmatic registration). Meta Bind's addButton is not idempotent — it throws on duplicate id. The error stack shows Tn.addButton → pY.onMount (component mount). Fix: pick one source of truth — keep the declaration or the template, never both. When migrating to programmatic registration, scrub all in-file meta-bind-button declarations of those ids and replace with bare BUTTON[id] references.

  12. Button ID not Found on first cold open, works on second. Race between Meta Bind's first render pass and your plugin's programmatic registration. Meta Bind renders already-open files before registration runs → BUTTON[id] resolves to nothing → failed render is cached in the markdown view. Fix: use the full three-step lifecycle in the "zero data.json" pattern below — sync register in onload, re-register on onLayoutReady, cleanup on onunload. The sync onload step is what prevents the bug.

Centralizing buttons via buttonTemplates

Inline references BUTTON[id] look up the button first in buttonTemplates (Meta Bind plugin settings, in .obsidian/plugins/obsidian-meta-bind-plugin/data.json), and only then in local declarations inside the same note. Source: getButton(file, id) { if (this.buttonTemplates.has(id)) return this.buttonTemplates.get(id); ... }.

Use this when: the same button is referenced from many notes, or you want notes to be free of YAML clutter.

data.json shape:

{
  "buttonTemplates": [
    {
      "id": "open-notes",
      "label": "📂 Open notes",
      "style": "primary",
      "hidden": true,
      "action": {
        "type": "inlineJS",
        "code": "const target = app.vault.getAbstractFileByPath('Notes.md');\nawait app.workspace.getLeaf(false).openFile(target);\n"
      }
    }
  ]
}

After adding templates, in any note you write only:

`BUTTON[open-notes]`

No meta-bind-button block needed.

Versioning: if the vault gitignores .obsidian/plugins/*/data.json, add an explicit exception for Meta Bind so templates travel with the vault:

.obsidian/plugins/*/data.json
!.obsidian/plugins/obsidian-meta-bind-plugin/data.json

Editing templates: Settings → Meta Bind → Button Templates in Obsidian, or edit data.json directly and reload the plugin. For bulk authoring, a script that writes to buttonTemplates is faster than the UI.

Pattern: thin templates + logic in another plugin

When the vault has a helper plugin, the cleanest split is:

  • data.json buttonTemplates holds presentation (label, style, icon, hidden flag) and a one-liner action that delegates to the helper plugin:
    "action": {
      "type": "inlineJS",
      "code": "await app.plugins.plugins['helper-plugin'].api.runButton('my-button');\n"
    }
    
  • Helper plugin holds the registry of what each button does — a map of { id → config } keyed by button id, where each config picks the underlying primitive (open file, write a bullet, open a modal, etc.).
  • A single dispatch function runButton(id) in the helper plugin reads the registry and calls the right primitive.

This keeps data.json minimal and uniform (every button is one line), keeps logic in normal JS files under git with refactor-friendly tooling, and survives Meta Bind upgrades without churn.

Pattern: zero data.json — programmatic registration from helper plugin

Goal: eliminate buttonTemplates from data.json entirely. The helper plugin owns one in-code map of { id → presentation } and pushes those into Meta Bind's buttonManager.buttonTemplates Map at startup.

Lifecycle has three steps; each lines up with a real failure mode. Use this exact shape.

StepWhenWhat it doesWhy it's needed
1onload (sync)Register templatesBeats Meta Bind to its first render of already-open files. Without this, BUTTON[id] shows "id not found" until the file is reopened.
2onLayoutReadyRegister again + rerender open reading-mode viewsCovers the case where Meta Bind wasn't loaded yet during step 1 (rare load-order edge case); previewMode.rerender(true) clears any cached placeholder.
3onunloadDelete our ids from the MapHot Reload of the helper plugin would otherwise leave stale templates that throw "already exists" on next register.
const MB_PLUGIN_ID = "obsidian-meta-bind-plugin";

class HelperPlugin extends Plugin {
  async onload() {
    this.api = { /* … */ };

    this.tryRegisterButtonTemplates();
    this.app.workspace.onLayoutReady(() => {
      this.tryRegisterButtonTemplates();
      this.refreshOpenMarkdownViews();
    });
  }

  tryRegisterButtonTemplates() {
    try { this.registerButtonTemplates(); }
    catch (e) { console.warn("[helper] meta-bind register failed:", e); }
  }

  registerButtonTemplates() {
    const map = this.app.plugins.getPlugin(MB_PLUGIN_ID)
      ?.api?.mb?.buttonManager?.buttonTemplates;  // INTERNAL API
    if (!map?.set) return;                        // Meta Bind not ready yet
    for (const id of Object.keys(MY_BUTTONS)) {
      map.set(id, {                               // .set replaces by key, idempotent
        id,
        label: MY_BUTTONS[id].label,
        style: MY_BUTTONS[id].style ?? "default",
        hidden: true,
        action: {
          type: "inlineJS",
          code: `await app.plugins.plugins['my-plugin'].api.fn('${id}');\n`,
        },
      });
    }
  }

  refreshOpenMarkdownViews() {
    this.app.workspace.iterateAllLeaves((leaf) => {
      const view = leaf?.view;
      if (view?.getViewType?.() !== "markdown") return;
      try { view.previewMode?.rerender?.(true); } catch (_) {}
    });
  }

  onunload() {
    try {
      const map = this.app.plugins.getPlugin(MB_PLUGIN_ID)
        ?.api?.mb?.buttonManager?.buttonTemplates;
      if (map) for (const id of Object.keys(MY_BUTTONS)) map.delete(id);
    } catch (_) { /* best effort */ }
    delete this.api;
  }
}

Caveats:

  • mb.api.mb.buttonManager.buttonTemplates is internal. Subject to break on Meta Bind upgrades; if a public setButtonTemplate / registerButtonTemplate ships, switch to that.
  • If the user opens Settings → Meta Bind → Button Templates and saves, Meta Bind re-runs loadTemplates() and wipes programmatic entries. Subscribe to settings-change events and re-register, or document the limitation.
  • data.json buttonTemplates: [] stays empty under git. Keep the gitignore exception (!.obsidian/plugins/obsidian-meta-bind-plugin/data.json) so the empty-array state travels with the vault.

Embedding inline JS that calls plugin APIs

When a button calls a custom plugin's API (e.g. app.plugins.plugins["my-plugin"].api.x.y(...)), bear in mind:

  • The plugin must be loaded; if it crashed during onload, the API is undefined and clicks no-op or throw Cannot read properties of undefined.
  • Wrap with optional chaining for graceful failure: app.plugins.plugins["x"]?.api?.fn?.().

Working patterns

Append a dated bullet to a section in the active file

Plain-JS write (no helper plugin) — useful when you don't want a registry but need a quick logger.

label: "✓ Log today"
id: log-today
hidden: true
style: primary
action:
  type: inlineJS
  code: |
    const file = app.workspace.getActiveFile();
    const today = window.moment().format("YYYY-MM-DD");
    const content = await app.vault.read(file);
    await app.vault.modify(file, content + `\n- ${today}`);
    new Notice("logged " + today);

For anything more structured (sections, dedupe, sorted insertion, multi-field input) use a helper plugin's primitives — see the modal pattern below.

Open or create a file via plugin helper

label: "📂 Open monthly log"
id: open-monthly
hidden: true
style: default
action:
  type: inlineJS
  code: |
    await app.plugins.plugins["helper-plugin"].api.openMonthly({ folder: "Activity", templatePath: "Templates/Monthly.md" });

Modal-based event capture (avoiding frontmatter buffers)

label: "🌸 Add event"
id: event-add
hidden: true
style: default
action:
  type: inlineJS
  code: |
    const helper = app.plugins.plugins["helper-plugin"].api;
    await helper.recordWithModal({
      file: app.workspace.getActiveFile(),
      section: "## Events",
      title: "Add event",
      successMessage: "Recorded",
      fixedFields: { type: "event" },
      fields: [
        { key: "item", label: "Item", type: "text", required: true },
        { key: "cost", label: "Cost", type: "number" }
      ]
    });

Quick checklist before shipping a button

  • label, style set (style ∈ enum).
  • id matches the inline BUTTON[id] reference exactly.
  • hidden: true if you only want the inline-rendered button.
  • action.type is one of the supported types.
  • If inlineJS — JS Engine plugin is enabled, enableJs: true, no trailing commas, single-line object literal preferred.
  • File is not under any excludedFolders path.
  • If the action calls a custom plugin, that plugin's api is reachable and the namespace function exists.
  • Cmd+P → Reload app without saving after schema or plugin changes.

Proactive use cases — when to suggest Meta Bind

Watch for these user phrasings or contexts and propose Meta Bind solutions:

User signalSuggest
"I want a button to do X" / "click to record/append"Meta Bind button with appropriate action type.
"I want to track {state} in this note" / status / completionINPUT[toggle] / INPUT[select] bound to frontmatter.
"show {value} from frontmatter" / "computed value"VIEW[…] math or text field.
"rate / score 1–5 / 1–10 in note"INPUT[slider(addLabels, minValue(N), maxValue(M)):rating].
"pick from a list of notes/tags"INPUT[suggester(optionQuery("#tag")):field].
"checklist for daily/weekly habit"INPUT[multiSelect] or several INPUT[toggle].
"this template should have working widgets in every copy"Plain INPUT/VIEW in the template — they bind to each instance's frontmatter automatically.
"I want a single source widget reused across notes"meta-bind-embed (rebinds widgets to host frontmatter) or buttonTemplates / inputFieldTemplates with reference.
"create a new note from this button"createNote or templaterCreateNote action.
"insert text below the button"insertIntoNote with selfEnd + 1.
"modify a frontmatter field on click"setMetadata / updateMetadata action.
"memory state that survives page reload but not Obsidian restart"bind target with memory^ storage type.
"shared counter / state across all notes"bind target with globalMemory^ storage type.
"computed columns / dashboard tied to frontmatter"createTableMountable via API, or VIEW[] fields with bind targets.
"interactive chart / chart that updates with note property"jsViewField (returns a DOM node from JS).

When suggesting, show the inline syntax first (one line) — it's cheap to drop into a note. Move to the code-block form when the user wants multiple arguments or class styling.

Diagnostics — fast bisect when a button fails

  1. Drop a clone of any known-working button into the broken file with a fresh id — rules out file-level / folder-level exclusion (e.g. excludedFolders).
  2. If the clone works but the original doesn't, simplify the original's body to a single new Notice("test") — rules out JS-side bugs.
  3. If still broken, remove fields one by one (icon, class, cssStyle, …) — Meta Bind's zod errors name field paths but occasionally mislabel; bisect is faster than introspection.
  4. Open the Meta Bind error overlay (the rendered card on the broken button) — the structured zod path almost always names the offending field.
  5. For "Button ID not Found" specifically: open the same file in a second tab to see if it works on a fresh render — distinguishes a cached failed render (race, see trap #12) from a missing template.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.