Building Plugins
Plugins are JavaScript modules that run inside NoteTalker 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 NoteTalker 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 NoteTalker 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 NoteTalker discovers and configures your plugin. Here’s a complete example:
{ "id": "pomodoro", "name": "Pomodoro Timer", "version": "1.0.0", "author": "NoteTalker 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 the plugin directory |
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 NoteTalker 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 NoteTalker 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 NoteTalker configuration via atlas.config.get/set |
network | Make outbound HTTP(S) requests via atlas.net.fetch(). Only http:// and https:// URLs are allowed. |
Lifecycle Hooks
Section titled “Lifecycle Hooks”Your main.js can optionally define any combination of four async lifecycle functions. All four are optional — define only the ones you need. A plugin that just registers a toolbar button on load can define only onLoad.
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 // NoteTalker 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 NoteTalker (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 manipulation (the plugin’smain.jsruns outside the DOM; useatlas.ui.*or a tab plugin’sindex.htmliframe when you need UI)fetch/XMLHttpRequest— Not available directly. Useatlas.net.fetch()with thenetworkpermission for outbound HTTP. Tab plugins (inside theirindex.htmliframe) can use the browserfetchAPI directly.
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 NoteTalker 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 NoteTalker. 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.net.* | Outbound HTTP(S) requests | network |
atlas.plugin.* | Logging, commands, plugin metadata | None |
atlas.manifest.* | Read-only manifest info | None |
atlas.apiVersion | API version string (e.g. "1.0") | 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": "NoteTalker 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 NoteTalker 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; } // NoteTalker 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 NoteTalker
- 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
Reload during development
Section titled “Reload during development”Editing main.js and want to see changes without restarting NoteTalker? Click the Reload button next to an enabled plugin in Settings → Plugins. This calls onDisable then onUnload, tears down all UI and commands, then re-reads the plugin from disk and calls onLoad → onEnable. Much faster than restarting the app.
Load errors are visible in the Plugin list
Section titled “Load errors are visible in the Plugin list”If a plugin fails to load — syntax error, missing permission, runtime crash in onLoad — the plugin row in Settings → Plugins shows a red Error badge and the error message inline. You don’t have to hunt through the dev console to find out what went wrong (though the console still has the full stack trace).
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": "NoteTalker 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'); // NoteTalker 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 NoteTalker 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.
Distributing Your Plugin
Section titled “Distributing Your Plugin”Once you’ve built and tested your plugin locally, you can share it so other people can install it. NoteTalker plugins install from a URL or a ZIP file (see Using Plugins), so distribution is simple: host the plugin wherever you like and share the link.
Package and host it
Section titled “Package and host it”- Build and test your plugin locally — Make sure it works correctly in your own NoteTalker installation
- Package the plugin folder — Zip the folder containing
plugin.json,main.js, and any other files your plugin needs - Host it anywhere public — A GitHub repository (with the ZIP attached to a release) is the most common choice, but any public URL that serves the ZIP works
Users install it by opening Settings > Plugins, clicking Install from URL/ZIP, and pasting your link or selecting the downloaded file.
Getting listed in the directory
Section titled “Getting listed in the directory”The community plugin directory at talkwithyournotes.com/plugins helps people discover plugins. Listing is optional — a plugin works exactly the same whether or not it’s in the directory.
To propose your plugin for the directory, send us the details through the feedback and contact channels:
- A public URL to the plugin ZIP or repository — this is where users download it from
- Your
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)
- A category that best describes your plugin
What we look for
Section titled “What we look for”Before adding a plugin to the directory, we check:
- 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 its description — The plugin must do what the description says it does
- A public download URL — Preferably an HTTPS GitHub URL (e.g.,
https://github.com/username/atlas-plugin-name) so users can review the source before installing
Sharing your plugin helps grow the NoteTalker plugin ecosystem and makes your work available to other NoteTalker 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 NoteTalker 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 distributing your plugin