Building Plugins
Plugins are JavaScript modules that run inside Atlas and extend it with new functionality. Every plugin gets access to the atlas API — a sandboxed object for interacting with the vault, UI, tools, and more. You write a plugin.json manifest and a main.js file, drop them in your vault, and enable the plugin in Settings.
This guide walks through building a real plugin — a Pomodoro Timer — from scratch. By the end you’ll understand the plugin structure, manifest format, lifecycle hooks, and core APIs.
Quick Start
Section titled “Quick Start”Let’s build the simplest possible plugin to make sure everything works.
1. Create the plugin folder
Section titled “1. Create the plugin folder”Plugins live in your vault at atlas/plugins/{plugin-id}/. Create a folder:
your-vault/└── atlas/ └── plugins/ └── hello/ ├── plugin.json └── main.js2. Write the manifest
Section titled “2. Write the manifest”Create plugin.json:
{ "id": "hello", "name": "Hello World", "version": "1.0.0", "author": "Your Name", "description": "A minimal plugin that shows a greeting", "main": "main.js", "permissions": ["ui_components"], "enabled": true}3. Write the plugin code
Section titled “3. Write the plugin code”Create main.js:
async function onLoad() { atlas.ui.addToolbarButton({ icon: '👋', tooltip: 'Say Hello', onClick: () => { atlas.ui.showNotification('Hello from my plugin!', 'success'); } });}4. Enable it
Section titled “4. Enable it”Open Atlas Settings > Plugins, find “Hello World” in the list, and toggle it on. You should see a 👋 button in the toolbar. Click it and you’ll get a notification.
That’s it — you’ve built a working plugin.
Plugin Structure
Section titled “Plugin Structure”Every plugin is a folder inside {vault}/atlas/plugins/ containing at minimum two files:
my-plugin/├── plugin.json # Required: plugin manifest├── main.js # Required: plugin logic (referenced by manifest)├── index.html # Optional: full UI panel (for tab plugins)├── data/ # Auto-created: per-plugin persistent storage└── ... # Any other files your plugin needsplugin.json— Tells Atlas what your plugin is and what it needsmain.js— Your plugin code, loaded and executed when the plugin is enabledindex.html— Only needed if your plugin has its own tab/panel UI (see Advanced Plugins)data/— Created automatically when your plugin writes data viaatlas.data.*
The plugin.json Manifest
Section titled “The plugin.json Manifest”The manifest is how Atlas discovers and configures your plugin. Here’s a complete example:
{ "id": "pomodoro", "name": "Pomodoro Timer", "version": "1.0.0", "author": "Atlas Team", "description": "Pomodoro technique timer with work/break intervals and task tracking", "main": "main.js", "min_atlas_version": "1.0.0", "permissions": [ "ui_components", "execute_tools", "write_vault" ], "enabled": true}Manifest Fields
Section titled “Manifest Fields”| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier for your plugin (lowercase, hyphens OK). Must match the folder name. |
name | Yes | Display name shown in the plugin list and marketplace |
version | Yes | Semantic version string (e.g. 1.0.0) |
author | Yes | Your name or organization |
description | Yes | Short description of what the plugin does |
main | Yes | Path to the JavaScript entry point (usually main.js) |
permissions | Yes | Array of permission strings the plugin needs |
enabled | No | Whether the plugin starts enabled (default: false) |
min_atlas_version | No | Minimum Atlas version required |
ui | No | Configuration for tab-based plugin UI (see Advanced Plugins) |
Permissions
Section titled “Permissions”Request only the permissions your plugin genuinely needs. Users see this list before enabling a plugin.
| Permission | What It Unlocks |
|---|---|
read_vault | Read files and list directories in the user’s vault via atlas.vault.* |
write_vault | Create, edit, and delete vault files via atlas.vault.* |
execute_tools | Call Atlas agent tools (search, create notes, add tasks) via atlas.tools.* |
ui_components | Add toolbar buttons, status bar items, modals, and notifications via atlas.ui.* |
config | Read and write global Atlas configuration via atlas.config.get/set |
network | Make outbound HTTP requests to external services |
Lifecycle Hooks
Section titled “Lifecycle Hooks”Your main.js defines async functions that Atlas calls at specific moments:
async function onLoad() { // Called when the plugin is first loaded // Set up UI elements, register commands, initialize state}
async function onEnable() { // Called after onLoad on startup, or when the user toggles the plugin on}
async function onDisable() { // Called when the user toggles the plugin off // Atlas auto-cleans your UI elements and commands, but stop // your own timers, intervals, or background work here}
async function onUnload() { // Called on app shutdown or plugin removal // Final cleanup opportunity}The typical call order:
- Plugin enabled →
onLoad()→onEnable() - User disables →
onDisable() - User re-enables →
onEnable() - App closes →
onDisable()→onUnload()
Plugin Runtime Environment
Section titled “Plugin Runtime Environment”Your main.js file is executed via a sandboxed Function() constructor, not as an ES module or CommonJS module. This means your plugin code runs in a controlled environment with limited access to browser APIs for security.
What IS Available
Section titled “What IS Available”Your plugin code can use:
atlasAPI object — The primary interface for interacting with Atlas (vault, UI, tools, etc.)console—console.log(),console.error(),console.warn()for debugging- Modern JavaScript syntax:
async/awaitfor asynchronous operationsconst/letfor variable declarations- Template literals (backticks)
- Arrow functions
- Classes
Promisefor async handlingsetTimeout/setInterval/clearTimeout/clearIntervalfor timers
What is NOT Available
Section titled “What is NOT Available”Your plugin code cannot directly access:
import/require— No ES module imports or CommonJS requires (your code is not a module)window— No direct access to the browser’s window objectdocument— No direct DOM manipulationfetch— No direct HTTP requests (useatlas.vault.*for vault files, requestnetworkpermission for external APIs in future versions)XMLHttpRequest— No XHR access
Tab Plugins Have Different Sandboxing
Section titled “Tab Plugins Have Different Sandboxing”If your plugin uses tab UI (has a ui block in plugin.json with an index.html file), the HTML page runs in a sandboxed iframe with a postMessage bridge. Inside the iframe, you DO have access to:
- Standard browser APIs (
document,fetch,XMLHttpRequest, etc.) - The iframe environment is isolated from the main Atlas window
- Communication with
main.jshappens via theatlasAPI bridge (postMessage under the hood)
See Advanced Plugins for details on building tab plugins.
The atlas API
Section titled “The atlas API”Every plugin gets access to a global atlas object. This is your interface to Atlas. The API is organized into namespaces:
| Namespace | Purpose | Permission Required |
|---|---|---|
atlas.vault.* | Read/write vault files | read_vault / write_vault |
atlas.ui.* | Toolbar buttons, status bar, modals, notifications | ui_components |
atlas.tools.* | Search, create notes, add tasks | execute_tools |
atlas.config.* | Plugin settings and global config | None (plugin settings) / config (global) |
atlas.data.* | Per-plugin persistent file storage | None |
atlas.plugin.* | Logging, commands, plugin metadata | None |
atlas.manifest.* | Read-only manifest info | None |
For complete method signatures and parameters, see the Plugin API Reference.
Tutorial: Building the Pomodoro Timer
Section titled “Tutorial: Building the Pomodoro Timer”Now let’s build something real. The Pomodoro Timer plugin will:
- Add a toolbar button and status bar countdown
- Show a modal to enter the current task
- Run 25-minute work sessions and 5-minute breaks
- Log completed pomodoros to the daily note
- Support a keyboard shortcut
Step 1: Create the manifest
Section titled “Step 1: Create the manifest”Create atlas/plugins/pomodoro/plugin.json:
{ "id": "pomodoro", "name": "Pomodoro Timer", "version": "1.0.0", "author": "Atlas Team", "description": "Pomodoro technique timer with work/break intervals and task tracking", "main": "main.js", "permissions": [ "ui_components", "execute_tools", "write_vault" ], "enabled": true}We need three permissions:
ui_components— for the toolbar button, status bar countdown, modal dialog, and notificationsexecute_tools— to log completed pomodoros as tasks in the daily notewrite_vault— required by the tools API to write to vault files
Step 2: Set up state and constants
Section titled “Step 2: Set up state and constants”Create atlas/plugins/pomodoro/main.js and start with the timer state:
// Pomodoro Timer Pluginlet statusItemId = null;let timerInterval = null;let currentMode = 'idle'; // 'idle', 'work', 'break'let remainingSeconds = 0;let completedPomodoros = 0;let currentTask = '';
const WORK_DURATION = 25 * 60; // 25 minutesconst BREAK_DURATION = 5 * 60; // 5 minutesconst LONG_BREAK_DURATION = 15 * 60; // 15 minutesStep 3: Add UI elements in onLoad
Section titled “Step 3: Add UI elements in onLoad”async function onLoad() { atlas.plugin.log('Pomodoro Timer plugin loaded');
// Add a toolbar button atlas.ui.addToolbarButton({ icon: '🍅', tooltip: 'Start/Stop Pomodoro Timer', onClick: toggleTimer });
// Add a status bar item for the countdown display statusItemId = atlas.ui.addStatusBarItem({ text: '🍅 Ready', tooltip: 'Pomodoro Timer - Click toolbar button to start' });
// Register a keyboard shortcut atlas.plugin.registerCommand({ id: 'toggle', name: 'Toggle Pomodoro Timer', hotkey: 'Ctrl+Shift+P', callback: toggleTimer });}What’s happening:
addToolbarButtonputs a clickable button in the Atlas toolbar. It returns a button ID (useful if you need to remove it later).addStatusBarItemadds a text element to the bottom status bar. We save the ID so we can update the text as the timer counts down.registerCommandbinds a keyboard shortcut. The hotkey format is modifier keys (Ctrl,Shift,Alt,Meta) joined with+and a final key.
Step 4: Show a modal and start the timer
Section titled “Step 4: Show a modal and start the timer”When the user clicks the button or presses the shortcut, we toggle between starting and stopping:
async function toggleTimer() { if (currentMode === 'idle') { await startWorkSession(); } else { stopTimer(); }}
async function startWorkSession() { // Show a modal dialog to ask what the user is working on const result = await atlas.ui.showModal({ title: 'Start Pomodoro', content: ` <div style="padding: 0.5rem;"> <label>What are you working on?</label> <input type="text" id="task-name" placeholder="Task description" style="width: 100%; margin-top: 0.5rem; padding: 0.5rem;" /> </div> `, buttons: [ { label: 'Cancel', value: 'cancel', type: 'secondary' }, { label: 'Start', value: 'start', type: 'primary' } ] });
if (result.value !== 'start') return;
currentTask = result.formData['task-name'] || 'Untitled task'; currentMode = 'work'; remainingSeconds = WORK_DURATION;
startTimer(); atlas.ui.showNotification('Work session started! Focus for 25 minutes.', 'info');}Key detail: showModal returns a Promise that resolves with { value, formData }. The value is which button was clicked (or 'dismiss' if the modal was closed). The formData object contains values from any <input> elements in the modal content, keyed by their id attribute.
Step 5: Timer logic and display updates
Section titled “Step 5: Timer logic and display updates”function startTimer() { if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => { remainingSeconds--; updateDisplay();
if (remainingSeconds <= 0) { handleTimerComplete(); } }, 1000);
updateDisplay();}
function stopTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } currentMode = 'idle'; remainingSeconds = 0; updateDisplay(); atlas.ui.showNotification('Timer stopped', 'info');}
function updateDisplay() { if (!statusItemId) return;
if (currentMode === 'idle') { atlas.ui.updateStatusBarItem(statusItemId, { text: `🍅 Ready (${completedPomodoros} done)`, tooltip: 'Click toolbar button to start a pomodoro' }); } else { const minutes = Math.floor(remainingSeconds / 60); const seconds = remainingSeconds % 60; const timeStr = `${minutes}:${seconds.toString().padStart(2, '0')}`; const emoji = currentMode === 'work' ? '🍅' : '☕'; atlas.ui.updateStatusBarItem(statusItemId, { text: `${emoji} ${timeStr}`, tooltip: `${currentMode === 'work' ? 'Working' : 'Break'}: ${currentTask}` }); }}updateStatusBarItem lets you change the text and tooltip of a status bar item after creation. Here we update it every second to show the countdown.
Step 6: Handle session transitions and log completions
Section titled “Step 6: Handle session transitions and log completions”function handleTimerComplete() { clearInterval(timerInterval); timerInterval = null;
if (currentMode === 'work') { atlas.ui.showNotification('Work session complete! Time for a break.', 'success'); startBreakSession(); } else { atlas.ui.showNotification('Break complete! Ready for another session?', 'info'); currentMode = 'idle'; remainingSeconds = 0; updateDisplay(); }}
function startBreakSession() { completedPomodoros++;
const isLongBreak = (completedPomodoros % 4) === 0; currentMode = 'break'; remainingSeconds = isLongBreak ? LONG_BREAK_DURATION : BREAK_DURATION;
startTimer();
const breakType = isLongBreak ? 'Long' : 'Short'; const duration = isLongBreak ? '15' : '5'; atlas.ui.showNotification( `${breakType} break! Rest for ${duration} minutes.`, 'success' );
// Log the completed pomodoro to the daily note logPomodoro(currentTask);}
async function logPomodoro(task) { try { await atlas.tools.addTask(`Completed pomodoro: ${task}`); atlas.plugin.log(`Logged pomodoro: ${task}`); } catch (error) { atlas.plugin.log(`Error logging pomodoro: ${error}`, 'error'); }}atlas.tools.addTask() adds a task line to the current daily note — the same as the agent’s add_task tool. The atlas.plugin.log() calls write to the browser console with a [Plugin: pomodoro] prefix, helpful for debugging.
Step 7: Clean up on disable
Section titled “Step 7: Clean up on disable”async function onDisable() { atlas.plugin.log('Pomodoro Timer plugin disabled'); if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } // Atlas automatically removes the toolbar button, status bar item, // and keyboard shortcut — no need to remove them manually}The complete plugin
Section titled “The complete plugin”That’s the full Pomodoro Timer. The complete main.js is about 100 lines of straightforward JavaScript. No build tools, no framework, no bundler — just a manifest and a JS file.
Testing Your Plugin
Section titled “Testing Your Plugin”- Place your plugin folder in
{vault}/atlas/plugins/ - Open Settings > Plugins in Atlas
- Your plugin should appear in the list — toggle it on
- Open the browser dev tools console (Ctrl+Shift+I) to see
atlas.plugin.log()output and any errors - If your plugin doesn’t appear, check that
plugin.jsonis valid JSON and theidmatches the folder name
Complete Example: Word Count Plugin
Section titled “Complete Example: Word Count Plugin”Here’s a complete, copy-pasteable example plugin that you can install and run immediately. This Word Count plugin scans all markdown files in your vault and displays the total word count in a modal when you click the toolbar button.
plugin.json
Section titled “plugin.json”Create {vault}/atlas/plugins/word-count/plugin.json:
{ "id": "word-count", "name": "Word Count", "version": "1.0.0", "author": "Atlas Community", "description": "Display total word count across all vault markdown files", "main": "main.js", "permissions": [ "read_vault", "ui_components" ], "enabled": true}main.js
Section titled “main.js”Create {vault}/atlas/plugins/word-count/main.js:
// Word Count Pluginlet toolbarButtonId = null;
async function onLoad() { atlas.plugin.log('Word Count plugin loaded');
// Add toolbar button toolbarButtonId = atlas.ui.addToolbarButton({ icon: '📊', tooltip: 'Show Word Count', onClick: showWordCount });}
async function onEnable() { atlas.plugin.log('Word Count plugin enabled');}
async function onDisable() { atlas.plugin.log('Word Count plugin disabled'); // Atlas auto-cleans UI elements, but you can do additional cleanup here}
async function onUnload() { atlas.plugin.log('Word Count plugin unloaded');}
async function showWordCount() { try { atlas.ui.showNotification('Counting words...', 'info');
// Get all files in vault root const items = await atlas.vault.list(''); let totalWords = 0; let fileCount = 0;
// Process all markdown files recursively totalWords = await countWordsInDirectory('', items);
// Count markdown files fileCount = await countMarkdownFiles('', items);
// Show results in modal atlas.ui.showModal({ title: 'Vault Word Count', content: ` <div style="padding: 1rem; text-align: center;"> <h2 style="margin: 0 0 1rem 0; font-size: 2rem; color: var(--accent);"> ${totalWords.toLocaleString()} </h2> <p style="margin: 0; color: var(--text-secondary);"> words across ${fileCount} markdown files </p> </div> `, buttons: [ { label: 'Close', value: 'close', type: 'primary' } ] });
atlas.plugin.log(`Total words: ${totalWords} in ${fileCount} files`); } catch (error) { atlas.ui.showNotification(`Error counting words: ${error}`, 'error'); atlas.plugin.log(`Error: ${error}`, 'error'); }}
async function countWordsInDirectory(path, items) { let totalWords = 0;
for (const item of items) { const itemPath = path ? `${path}/${item.name}` : item.name;
if (item.isDirectory) { // Recursively process subdirectories const subItems = await atlas.vault.list(itemPath); totalWords += await countWordsInDirectory(itemPath, subItems); } else if (item.name.endsWith('.md')) { // Count words in markdown files try { const content = await atlas.vault.read(itemPath); const words = content.split(/\s+/).filter(word => word.length > 0); totalWords += words.length; } catch (error) { atlas.plugin.log(`Could not read ${itemPath}: ${error}`, 'warn'); } } }
return totalWords;}
async function countMarkdownFiles(path, items) { let count = 0;
for (const item of items) { const itemPath = path ? `${path}/${item.name}` : item.name;
if (item.isDirectory) { const subItems = await atlas.vault.list(itemPath); count += await countMarkdownFiles(itemPath, subItems); } else if (item.name.endsWith('.md')) { count++; } }
return count;}Installation
Section titled “Installation”- Copy the code above into the two files at the paths shown
- Open Atlas Settings > Plugins
- Find “Word Count” in the list and toggle it on
- Click the 📊 button in the toolbar to see your vault’s word count
This example demonstrates:
- All four lifecycle hooks (
onLoad,onEnable,onDisable,onUnload) - Reading vault files with
atlas.vault.read()andatlas.vault.list() - Recursive directory traversal
- Modal dialogs with styled content
- Notifications for user feedback
- Plugin logging for debugging
- Proper cleanup patterns
The plugin uses bare function declarations (no export keyword) as shown in the runtime environment documentation. It’s a complete, working plugin that you can use as a starting point for your own creations.
Publishing to the Marketplace
Section titled “Publishing to the Marketplace”Once you’ve built and tested your plugin locally, you can submit it to the Atlas Plugin Marketplace so other users can discover and install it.
Submission Flow
Section titled “Submission Flow”- Build and test your plugin locally — Ensure it works correctly in your own Atlas installation
- Host it on a public GitHub repository — Your plugin code must be publicly accessible
- Submit via the cloud backend — Use the
POST /pluginsendpoint to submit your plugin listing
Submission Requirements
Section titled “Submission Requirements”Your plugin submission must include:
- Complete
plugin.jsonmanifest with all required fields:id(unique identifier, lowercase with hyphens)name(display name)version(semantic versioning, e.g.,1.0.0)author(your name or organization)description(clear, concise description of functionality)permissions(accurate list of required permissions)
- Public GitHub repository URL — This goes in the
download_urlfield. Users download your plugin from GitHub, then install it via Settings. - Category — Choose a category that best describes your plugin (see the marketplace for available categories)
Review Process
Section titled “Review Process”- Submission — When you submit via
POST /plugins, your plugin goes intopendingstatus - Review — The Atlas team reviews your plugin for security, quality, and accuracy
- Approval — Once approved, your plugin appears in the public marketplace at atlas.app/plugins and in the in-app Marketplace tab
- Updates — You can update your plugin listing at any time via
PUT /plugins/{id}. Updates also go through review.
Review Criteria
Section titled “Review Criteria”The Atlas team reviews plugins based on:
- No malicious code — Plugins must not contain malicious behavior, data exfiltration, or security vulnerabilities
- Accurate permission declarations — The
permissionsarray must accurately reflect what your plugin actually does. Don’t request permissions you don’t use. - Working plugin that matches description — The plugin must do what the description says it does
- HTTPS download URL — The
download_urlmust be a public HTTPS GitHub repository URL (e.g.,https://github.com/username/atlas-plugin-name)
Plugin Ratings and Discovery
Section titled “Plugin Ratings and Discovery”After your plugin is approved:
- Users can rate and review your plugin (1-5 stars + optional text review)
- Highly-rated plugins appear higher in marketplace search results
- Download counts are tracked and displayed on your plugin’s marketplace page
- Users can search for plugins by name, description, and category
Publishing your plugin to the marketplace helps grow the Atlas plugin ecosystem and makes your work available to thousands of Atlas users. Happy building!
Troubleshooting
Section titled “Troubleshooting”’Unexpected token export’
Section titled “’Unexpected token export’”Problem: You’re seeing a syntax error about export when your plugin loads.
Solution: Export keywords are optional and automatically stripped by the runtime. If you’re still seeing issues, use bare function declarations:
// ✅ Correctasync function onLoad() { ... }
// ❌ Not needed (but works)export async function onLoad() { ... }‘module is not defined’
Section titled “‘module is not defined’”Problem: You’re getting an error about module not being defined, or module.exports.
Solution: Plugins don’t use CommonJS or ES modules. Use bare function declarations instead of module.exports:
// ❌ Wrong - this is CommonJSmodule.exports = { onLoad: async function() { ... }}
// ✅ Correct - bare functionsasync function onLoad() { ... }Plugin not appearing after install
Section titled “Plugin not appearing after install”Problem: You installed a plugin but it doesn’t show up in the plugin list.
Solution: Check these common issues:
- The folder name must exactly match the
idfield inplugin.json(case-sensitive) - Make sure
plugin.jsonis valid JSON (use a JSON validator if unsure) - The plugin folder must be inside
{vault}/atlas/plugins/ - Try restarting Atlas or toggling Settings > Plugins off and on
Plugin disappears after enable/disable
Section titled “Plugin disappears after enable/disable”Problem: Your plugin appears in the list, but when you enable/disable it, it disappears or stops working.
Solution: This is almost always a JavaScript syntax error in your main.js. Open the browser dev tools console (Ctrl+Shift+I or Cmd+Option+I on Mac) and look for error messages prefixed with [Plugin: your-plugin-id]. Common issues:
- Missing closing braces
}or parentheses) - Using an API method that doesn’t exist (check Plugin API Reference)
- Using an API that requires a permission you didn’t declare in
plugin.json
Duplicate UI elements (two buttons or tabs)
Section titled “Duplicate UI elements (two buttons or tabs)”Problem: Your plugin is showing up twice in the UI — two toolbar buttons or two tabs.
Solution: If your plugin has a ui block in plugin.json and calls addToolbarButton() in your code, you’ll get duplicate UI elements. Choose one approach:
- Use
uiconfig inplugin.jsonfor simple tab plugins (noaddToolbarButton()needed) - Use
addToolbarButton()for toolbar-only plugins (nouiblock needed) - If you want both, only call
addToolbarButton()and don’t use theuiconfig
Tab plugin cannot call main.js functions
Section titled “Tab plugin cannot call main.js functions”Problem: Your tab plugin (index.html) is trying to call functions defined in main.js but they’re not accessible.
Solution: Tab plugins run in an isolated iframe and can’t directly call main.js functions. Use one of these patterns for communication:
Option 1: Use atlas.data as a message bus:
// In main.jsawait atlas.data.write('shared-state.json', JSON.stringify({ counter: 42 }));
// In index.html (iframe)const state = JSON.parse(await atlas.data.read('shared-state.json'));Option 2: Use atlas.plugin.emit() and atlas.plugin.on() for event-based communication (available in future releases).
Next Steps
Section titled “Next Steps”- Plugin API Reference — Complete reference for every
atlas.*method with parameters and return values - Advanced Plugins — Tab plugins with full HTML UI, the HTTP server API for external tools, per-plugin data storage, and publishing to the marketplace