User Scripts
User scripts extend QuickAdd’s functionality with custom code. They can be used within macros to perform complex operations, integrate with external APIs, and automate sophisticated workflows. A user script can be a standalone .js file or a ```js code block inside a regular note — the latter is editable on mobile, where Obsidian cannot open .js files.
Obsidian API Reference: This guide references the Obsidian API. Familiarize yourself with the App, Vault, and Workspace modules for advanced scripting.
Adding Scripts to Macros
Section titled “Adding Scripts to Macros”A user script can live in either of two places inside your vault:
- a standalone
.jsfile, or - a note (
.md) containing a```js(or```javascript) code block.
Avoid .obsidian and hidden dot-folders such as .scripts, because Obsidian
may exclude them from the vault file index QuickAdd uses for script discovery.
In the Macro Builder, Browse opens QuickAdd’s picker of discovered scripts
(both .js files and notes that contain a code block); it is not a native file
picker. If you add a script manually, type a .js script’s basename — for
scripts/my-script.js, enter my-script — or, for a note, type its vault path
(e.g. Scripts/my-script.md). For a specific export, append a member expression
such as my-script::start (or Scripts/my-script.md::start).
Scripts in a note code block
Section titled “Scripts in a note code block”Write your script in a ```js code block inside any note; QuickAdd runs the
first ```js (or ```javascript) block in the note and ignores all
prose and other code blocks. The block is a CommonJS module exactly like a .js
file — it must assign module.exports / exports.default (a top-level return
the way inline scripts work will not export anything):
# My script
Notes about what this does — ignored by QuickAdd.
```jsmodule.exports = async (params) => { // Your code here};```If the note has no ```js block, QuickAdd shows a notice and skips the script.
Basic Structure
Section titled “Basic Structure”Every user script must export a module with at least an entry point function:
module.exports = async (params) => { // Your code here};Or with the object syntax:
module.exports = { entry: start, settings: { name: "Script Name", author: "Your Name", options: { // Define configurable options here } }};
async function start(params, settings) { // Your code here}Sharing Code Between Scripts
Section titled “Sharing Code Between Scripts”Need the same helper functions or constants in several scripts? Put them in one
shared .js module and require() it from each script. User scripts run as
CommonJS modules and receive a require function (backed by Obsidian’s
Electron/Node require), so you can load any module on disk by its absolute
path.
Create the shared module
Section titled “Create the shared module”Save a normal .js file in your vault. (It must be a real .js file — require
loads from disk and cannot read a script written in a ```js note code block.)
Export your helpers with module.exports:
module.exports = { formatNumber: (n) => new Intl.NumberFormat("en-US").format(n), CURRENCY: "USD",};The module must already exist on disk when the requiring script runs.
Require it from your scripts
Section titled “Require it from your scripts”Build the absolute path at runtime from the vault’s base path, then require
the module. Don’t paste a literal absolute path — the vault lives at a different
location on every machine and account, so a hardcoded path breaks the moment the
vault is synced or opened elsewhere. Relative paths don’t work either (the script
has no __dirname), so always resolve against getBasePath():
module.exports = async (params) => { const { app, obsidian } = params;
// require() is desktop-only — fail clearly on mobile. if (!(app.vault.adapter instanceof obsidian.FileSystemAdapter)) { throw new Error("This script shares helpers via require(), which only works in the desktop app."); }
const path = require("path"); // getBasePath() is the supported method; older scripts may use the // equivalent app.vault.adapter.basePath. const utils = require(path.join(app.vault.adapter.getBasePath(), "scripts", "shared-utils.js"));
new obsidian.Notice(`${utils.formatNumber(1234567)} ${utils.CURRENCY}`); // "1,234,567 USD"};Cross-platform alternatives
Section titled “Cross-platform alternatives”Because require is desktop-only, on mobile (or for a portable vault) share code
another way:
- Inline the helper in each script, or
- Chain scripts in a macro and pass data (not functions) through
params.variablesbetween steps.
Script Parameters
Section titled “Script Parameters”The script receives two parameters:
params Object
Section titled “params Object”Contains the QuickAdd API and Obsidian context:
{ app: App, // Obsidian app instance - see https://docs.obsidian.md/Reference/TypeScript+API/App quickAddApi: QuickAddApi, // QuickAdd API methods (documented below) variables: {}, // Variables object for sharing data between scripts and templates obsidian: obsidian, // Obsidian module with all classes and utilities abort: (message) => never // Abort macro execution with optional message}The app object provides access to the entire Obsidian API, including:
app.vault- File and folder operations (Vault API)app.workspace- Window and pane management (Workspace API)app.metadataCache- File metadata and links (MetadataCache API)app.fileManager- File operations and renaming (FileManager API)
settings Object
Section titled “settings Object”Contains the user-configured values for your script’s options (only available when using the settings structure).
Configurable Options
Section titled “Configurable Options”User scripts can define configurable options that users can set through the QuickAdd UI. This makes your scripts more flexible and reusable.
Option Types
Section titled “Option Types”Text Input
Section titled “Text Input”For regular string values, paths, names, etc.
options: { "Project": { type: "text", // or "input" defaultValue: "", placeholder: "Project name", description: "Default project" // Optional: help text }}Secret Input
Section titled “Secret Input”For API keys, access tokens, and other sensitive values. Secrets are stored in Obsidian’s SecretStorage and QuickAdd stores only a reference in data.json.
options: { "API Key": { type: "secret", id: "api-key", // Optional stable storage id placeholder: "Paste API key", description: "Your API key" }}type: "text" / type: "input" with secret: true is still supported for older scripts and is treated as a secret setting.
Secret values are local to the Obsidian app profile. They are not included in QuickAdd package exports and are not synced through the plugin’s data.json; users must enter them on each device where the script runs.
The optional id controls the SecretStorage reference used for the setting. If omitted, QuickAdd uses the option name. Set id when you want to rename the visible setting label later without creating a new saved secret.
Toggle/Checkbox
Section titled “Toggle/Checkbox”For boolean on/off settings.
options: { "Enable Feature": { type: "toggle", // or "checkbox" defaultValue: false, description: "Enable this feature" }}Dropdown/Select
Section titled “Dropdown/Select”For choosing from predefined options.
options: { "Output Format": { type: "dropdown", // or "select" defaultValue: "markdown", options: ["markdown", "plain", "html"], description: "Choose output format" }}Format Input
Section titled “Format Input”For template strings with QuickAdd format syntax support.
options: { "File Name Template": { type: "format", defaultValue: "{{DATE}} - {{VALUE:title}}", placeholder: "Enter template", description: "Template for file names" }}Complete Example
Section titled “Complete Example”Here’s a comprehensive example showing all option types:
module.exports = { entry: start, settings: { name: "Advanced Script", author: "Your Name", options: { "API Key": { type: "secret", placeholder: "sk-...", description: "OpenAI API key" }, "Model": { type: "dropdown", defaultValue: "gpt-3.5-turbo", options: ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"], description: "AI model to use" }, "Include Metadata": { type: "toggle", defaultValue: true, description: "Include file metadata in output" }, "Output Template": { type: "format", defaultValue: "## {{DATE:YYYY-MM-DD}}\n{{VALUE:content}}", placeholder: "Template for output", description: "Format for the output" }, "Max Results": { type: "text", defaultValue: "10", placeholder: "Number", description: "Maximum number of results" } } }};
async function start(params, settings) { const { quickAddApi, app, variables } = params;
// Access settings const apiKey = settings["API Key"]; const model = settings["Model"]; const includeMetadata = settings["Include Metadata"]; const outputTemplate = settings["Output Template"]; const maxResults = parseInt(settings["Max Results"]);
// Validate inputs if (!apiKey) { new Notice("Please configure your API key in the script settings"); throw new Error("API key not configured"); }
// Use QuickAdd API const query = await quickAddApi.inputPrompt("Enter your search query:"); if (!query) return;
// Your logic here... console.log(`Using model: ${model}`); console.log(`Max results: ${maxResults}`);
if (includeMetadata) { // Include metadata logic }
// Set variables for use in templates variables.model = model; variables.query = query; variables.resultCount = maxResults;}Using the QuickAdd API
Section titled “Using the QuickAdd API”User scripts have full access to the QuickAdd API through params.quickAddApi. For complete API documentation, see the QuickAdd API Reference.
User Input
Section titled “User Input”// Text inputconst text = await quickAddApi.inputPrompt("Enter text:");
// Wide text input (multi-line)const longText = await quickAddApi.wideInputPrompt("Enter description:");
// Yes/No confirmationconst confirmed = await quickAddApi.yesNoPrompt("Are you sure?");
// Suggester (dropdown selection)const choice = await quickAddApi.suggester( ["Option 1", "Option 2", "Option 3"], ["value1", "value2", "value3"]);
// Checkbox selectionconst selected = await quickAddApi.checkboxPrompt( ["Item 1", "Item 2", "Item 3"], ["Item 1"] // Pre-selected items);Variables
Section titled “Variables”Set variables that can be used in subsequent template operations:
// Set variablesparams.variables.myVariable = "value";params.variables.timestamp = new Date().toISOString();params.variables.results = arrayOfResults;
// Variables are accessible in templates as {{VALUE:myVariable}}Formatting
Section titled “Formatting”Format strings with QuickAdd syntax:
const formatted = await quickAddApi.format( "Today is {{DATE}} and the title is {{VALUE:title}}", { title: "My Document" });Execute Other Choices
Section titled “Execute Other Choices”Trigger other QuickAdd choices programmatically:
await quickAddApi.executeChoice("My Other Choice", { customVariable: "value"});Error Handling and Macro Control
Section titled “Error Handling and Macro Control”Aborting Macro Execution
Section titled “Aborting Macro Execution”You can intentionally stop a macro using params.abort(). This is useful for validation or conditional execution:
module.exports = async (params) => { const { abort, quickAddApi } = params;
// Get user input const projectName = await quickAddApi.inputPrompt("Project name:");
// Validate input if (!projectName || projectName.length < 3) { abort("Project name must be at least 3 characters"); // Execution stops here - remaining macro commands won't run }
// Continue with valid input console.log(`Creating project: ${projectName}`);};When to use params.abort():
- Input validation failures
- Missing required configuration
- You want to provide a custom message after catching a
MacroAbortError - Prerequisites not met (e.g., required plugin not installed)
Prompt cancellations already throw MacroAbortError and halt macros automatically, so only call abort() in those scenarios if you need to surface a custom message or you’re stopping for a non-prompt reason.
What happens when you call abort():
- Macro execution stops immediately
- A message is logged: “Macro execution aborted: [your message]”
- Remaining commands in the macro are skipped
- No error is thrown to the user
QuickAdd API methods that can be cancelled:
inputPrompt()wideInputPrompt()yesNoPrompt()suggester()checkboxPrompt()
Each of these now rejects with MacroAbortError("Input cancelled by user") when the user presses Escape or closes the dialog. If you do nothing, the macro will automatically stop (matching user expectations). If you want to handle cancellation in your script, wrap the call in try/catch and intercept the error before it reaches the macro engine.
try { const name = await quickAddApi.inputPrompt("Your name:");} catch (error) { if (error?.name === "MacroAbortError") { // Optional custom handling (e.g., cleanup) before the macro aborts return; } throw error; // real errors should still bubble up}Important: Because cancellations now throw, you should only call abort() yourself when you want to provide a custom message or stop execution for a non-prompt reason.
module.exports = async (params) => { const { quickAddApi, abort } = params;
let name; try { name = await quickAddApi.inputPrompt("Your name:"); } catch (error) { if (error?.name === "MacroAbortError") { abort("Name is required"); return; } throw error; }
console.log(`Processing: ${name}`);};Script Error Handling
Section titled “Script Error Handling”Always include proper error handling for unexpected errors:
async function start(params, settings) { try { // Your code here const result = await riskyOperation();
if (!result) { new Notice("Operation failed", 5000); throw new Error("Failed to complete operation"); }
} catch (error) { console.error("Script error:", error); new Notice(`Error: ${error.message}`, 5000); throw error; // Re-throw to stop macro execution }}Error behavior:
- Unhandled errors in scripts automatically stop the macro
- Original error stack traces are preserved for debugging
- Errors are logged to the console for troubleshooting
Best Practices
Section titled “Best Practices”- Validate Settings: Always check that required settings are configured before using them
- User Feedback: Use
new Notice()to provide feedback to users - Error Messages: Provide clear, actionable error messages
- Default Values: Always provide sensible default values for options
- Documentation: Add descriptions to all options to help users understand their purpose
- Variable Names: Use clear, descriptive names for variables you set
- Async/Await: Always use async/await for asynchronous operations
- Console Logging: Use
console.log()for debugging, but remove or minimize in production
Advanced Patterns
Section titled “Advanced Patterns”Multiple Entry Points
Section titled “Multiple Entry Points”Export an object with multiple functions that users can choose from:
module.exports = { "Create Note": createNote, "Update Note": updateNote, "Delete Note": deleteNote};
async function createNote(params) { // Implementation}
async function updateNote(params) { // Implementation}
async function deleteNote(params) { // Implementation}Returning Values
Section titled “Returning Values”Scripts can return values that become available as macro output:
async function start(params, settings) { const result = await processData();
// Return value becomes available as macro output return result;}Working with Files
Section titled “Working with Files”async function start(params, settings) { const { app, obsidian } = params;
// Get all markdown files const files = app.vault.getMarkdownFiles();
// Get a specific file const file = app.vault.getAbstractFileByPath("path/to/file.md"); if (file instanceof obsidian.TFile) { // Read file content const content = await app.vault.read(file);
// Get file metadata const metadata = app.metadataCache.getFileCache(file); const frontmatter = metadata?.frontmatter; const links = metadata?.links || []; const tags = metadata?.tags || [];
// Process content const modified = content.replace(/old/g, "new");
// Save changes await app.vault.modify(file, modified); }
// Create new file const newFile = await app.vault.create( "folder/subfolder/new-note.md", "# Title\n\nContent here" );
// Rename file await app.fileManager.renameFile( newFile, "folder/subfolder/renamed-note.md" );
// Delete file await app.vault.delete(newFile);
// Copy file await app.vault.copy( file, "path/to/copy.md" );
// Get folder const folder = app.vault.getAbstractFileByPath("folder/subfolder"); if (folder instanceof obsidian.TFolder) { // List folder contents const children = folder.children;
// Create folder if it doesn't exist const path = "new/folder/structure"; if (!app.vault.getAbstractFileByPath(path)) { await app.vault.createFolder(path); } }}Working with the Active File
Section titled “Working with the Active File”async function start(params, settings) { const { app, obsidian } = params;
// Get active file const activeFile = app.workspace.getActiveFile(); if (!activeFile) { new obsidian.Notice("No active file"); return; }
// Get active editor const activeView = app.workspace.getActiveViewOfType(obsidian.MarkdownView); if (activeView) { const editor = activeView.editor;
// Get selected text const selection = editor.getSelection();
// Get current line const cursor = editor.getCursor(); const line = editor.getLine(cursor.line);
// Replace selection editor.replaceSelection("New text");
// Insert at cursor editor.replaceRange( "Inserted text", cursor );
// Get entire document const fullText = editor.getValue();
// Replace entire document editor.setValue("Completely new content"); }}Working with Metadata and Frontmatter
Section titled “Working with Metadata and Frontmatter”async function start(params, settings) { const { app, obsidian } = params;
const file = app.workspace.getActiveFile(); if (!file) return;
// Get frontmatter const cache = app.metadataCache.getFileCache(file); const frontmatter = cache?.frontmatter || {};
// Update frontmatter await app.fileManager.processFrontMatter(file, (fm) => { fm.tags = fm.tags || []; fm.tags.push("processed"); fm.date = new Date().toISOString(); fm.status = "completed"; delete fm.oldField; // Remove a field });
// Get all files with specific frontmatter const filesWithTag = app.vault.getMarkdownFiles().filter(f => { const meta = app.metadataCache.getFileCache(f); return meta?.frontmatter?.tags?.includes("important"); });
// Get backlinks const backlinks = app.metadataCache.getBacklinksForFile(file);
// Get outgoing links const links = cache?.links || []; const embeds = cache?.embeds || [];}Opening and Navigating Files
Section titled “Opening and Navigating Files”async function start(params, settings) { const { app, obsidian } = params;
// Open file in active pane const file = app.vault.getAbstractFileByPath("path/to/note.md"); if (file instanceof obsidian.TFile) { await app.workspace.getLeaf().openFile(file); }
// Open in new pane await app.workspace.getLeaf('split').openFile(file);
// Open in new tab await app.workspace.getLeaf('tab').openFile(file);
// Open in new window await app.workspace.getLeaf('window').openFile(file);
// Navigate to specific heading await app.workspace.openLinkText( "note#heading", "", // source path true // new leaf );
// Create and open a daily note const { createDailyNote } = app.plugins.plugins["daily-notes"].instance; const dailyNote = await createDailyNote(moment()); await app.workspace.getLeaf().openFile(dailyNote);}Common Patterns & Recipes
Section titled “Common Patterns & Recipes”Processing Multiple Notes
Section titled “Processing Multiple Notes”async function processAllNotesInFolder(params, settings) { const { app, obsidian, quickAddApi } = params;
const folderPath = settings["Folder Path"] || "Notes"; const folder = app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof obsidian.TFolder)) { throw new Error(`Folder not found: ${folderPath}`); }
let processed = 0; const errors = [];
// Process each markdown file in folder for (const file of folder.children) { if (file instanceof obsidian.TFile && file.extension === "md") { try { const content = await app.vault.read(file);
// Your processing logic here const modified = content + "\n\n---\nProcessed: " + new Date().toISOString();
await app.vault.modify(file, modified); processed++;
} catch (error) { errors.push(`${file.path}: ${error.message}`); } } }
// Report results new obsidian.Notice(`Processed ${processed} files`); if (errors.length > 0) { await quickAddApi.infoDialog("Errors", errors); }
return { processed, errors };}Creating Notes from Templates
Section titled “Creating Notes from Templates”async function createNoteFromTemplate(params, settings) { const { app, quickAddApi, variables } = params;
// Get template const templatePath = settings["Template Path"]; const templateFile = app.vault.getAbstractFileByPath(templatePath);
if (!templateFile) { throw new Error(`Template not found: ${templatePath}`); }
// Read template content const template = await app.vault.read(templateFile);
// Get user input const title = await quickAddApi.inputPrompt("Note title:"); const tags = await quickAddApi.inputPrompt("Tags (comma-separated):");
// Format the template const formatted = await quickAddApi.format(template, { title: title, tags: tags.split(",").map(t => `#${t.trim()}`).join(" "), date: new Date().toISOString() });
// Create the note const fileName = `${title.replace(/[^\w\s]/gi, '')}.md`; const filePath = `${settings["Output Folder"]}/${fileName}`;
await app.vault.create(filePath, formatted);
// Open the new note const newFile = app.vault.getAbstractFileByPath(filePath); await app.workspace.getLeaf().openFile(newFile);
return filePath;}Bulk Tag Operations
Section titled “Bulk Tag Operations”async function bulkTagOperations(params, settings) { const { app, quickAddApi, obsidian } = params;
const operation = await quickAddApi.suggester( ["Add tag", "Remove tag", "Replace tag"], ["add", "remove", "replace"] );
const tag = await quickAddApi.inputPrompt("Tag name (without #):"); let newTag;
if (operation === "replace") { newTag = await quickAddApi.inputPrompt("Replace with tag:"); }
const files = app.vault.getMarkdownFiles(); let modified = 0;
for (const file of files) { await app.fileManager.processFrontMatter(file, (fm) => { fm.tags = fm.tags || [];
if (operation === "add" && !fm.tags.includes(tag)) { fm.tags.push(tag); modified++; } else if (operation === "remove") { const index = fm.tags.indexOf(tag); if (index > -1) { fm.tags.splice(index, 1); modified++; } } else if (operation === "replace") { const index = fm.tags.indexOf(tag); if (index > -1) { fm.tags[index] = newTag; modified++; } } }); }
new obsidian.Notice(`Modified ${modified} files`); return modified;}Search and Replace Across Vault
Section titled “Search and Replace Across Vault”async function searchAndReplace(params, settings) { const { app, quickAddApi, obsidian } = params;
const searchTerm = await quickAddApi.inputPrompt("Search for:"); const replaceTerm = await quickAddApi.inputPrompt("Replace with:");
const confirm = await quickAddApi.yesNoPrompt( "Confirm", `Replace all occurrences of "${searchTerm}" with "${replaceTerm}"?` );
if (!confirm) return;
const files = app.vault.getMarkdownFiles(); const results = [];
for (const file of files) { const content = await app.vault.read(file);
if (content.includes(searchTerm)) { const newContent = content.replaceAll(searchTerm, replaceTerm); await app.vault.modify(file, newContent);
const count = (content.match(new RegExp(searchTerm, 'g')) || []).length; results.push(`${file.path}: ${count} replacements`); } }
if (results.length > 0) { await quickAddApi.infoDialog("Replacements Made", results); } else { new obsidian.Notice("No matches found"); }
return results;}Daily Note Automation
Section titled “Daily Note Automation”async function enhanceDailyNote(params, settings) { const { app, obsidian } = params;
// Get or create today's daily note const { moment } = window; const dailyNotes = app.plugins.plugins["daily-notes"];
if (!dailyNotes) { throw new Error("Daily Notes plugin not enabled"); }
const { createDailyNote, getDailyNote } = dailyNotes.instance; const date = moment();
let dailyNote = getDailyNote(date, dailyNotes.instance.options); if (!dailyNote) { dailyNote = await createDailyNote(date); }
// Add content to daily note const content = await app.vault.read(dailyNote);
// Add weather (example - would need actual API) const weather = "☀️ Sunny, 22°C";
// Add tasks from yesterday const yesterday = moment().subtract(1, 'day'); const yesterdayNote = getDailyNote(yesterday, dailyNotes.instance.options);
let unfinishedTasks = ""; if (yesterdayNote) { const yesterdayContent = await app.vault.read(yesterdayNote); const taskRegex = /- \[ \] .+/g; const tasks = yesterdayContent.match(taskRegex); if (tasks) { unfinishedTasks = "\n## Carried Over Tasks\n" + tasks.join("\n"); } }
const enhanced = content + `## Weather${weather}
${unfinishedTasks}
## Notes-
## Gratitude-`;
await app.vault.modify(dailyNote, enhanced); await app.workspace.getLeaf().openFile(dailyNote);
return dailyNote.path;}Debugging Tips
Section titled “Debugging Tips”-
Use Console Logging Strategically
console.log("Script started", { settings, params });console.group("Processing files");files.forEach(f => console.log(f.path));console.groupEnd(); -
Developer Console Access
- Windows/Linux:
Ctrl + Shift + I - Mac:
Cmd + Option + I - Filter console by typing “QuickAdd” to see only relevant logs
- Windows/Linux:
-
Error Boundaries
try {// Risky operationawait app.vault.modify(file, content);} catch (error) {console.error("Failed to modify file:", error);new Notice(`Error: ${error.message}`);// Continue or throw depending on severity} -
Performance Monitoring
console.time("Processing files");// ... your codeconsole.timeEnd("Processing files"); -
Debugging State
// Use debugger statement to pause executiondebugger; // Execution will pause here if DevTools is open// Inspect variables at specific pointsconsole.table(variables); // Great for objects/arrays
Example Scripts
Section titled “Example Scripts”For complete working examples, see:
- Complete Example with All Options - Demonstrates all option types and patterns
- Movie Script - Fetches movie data from OMDb API
- Citations Manager - Integrates with Obsidian Citations plugin
- Book Finder - Searches for book information
- Migrate Dataview Properties - Migrates inline dataview properties to YAML frontmatter with configurable settings
Additional Resources
Section titled “Additional Resources”Obsidian API Documentation
Section titled “Obsidian API Documentation”- Official Obsidian API Reference
- Plugin Development Guide
- Vault Module - File operations
- MetadataCache - File metadata and links
- Workspace - Panes and leaves
- Editor - Text editing operations
QuickAdd Resources
Section titled “QuickAdd Resources”Troubleshooting
Section titled “Troubleshooting”Script not loading:
- Check the file path in your macro configuration
- Ensure the script exports a valid module
- Check console for syntax errors
Settings not appearing:
- Verify the settings object structure
- Ensure option names are unique
- Restart Obsidian after making changes to the settings structure
Variables not available in templates:
- Make sure you’re setting them on
params.variables - Read them with
{{VALUE:name}}, not{{name}} - Check that the script completes successfully
- See Variables and data flow for how script variables move into later Template and Capture steps
API methods returning undefined:
- Ensure you’re using
awaitwith async methods - Check that QuickAdd plugin is enabled
- Verify you’re accessing the API correctly through
params.quickAddApi