Skip to content

Advanced Plugins

The plugins covered in Building Plugins run as background scripts — they add toolbar buttons, status bar items, and modals, but don’t have their own dedicated UI panel. Tab plugins go further: they get their own tab in Atlas with a full HTML interface rendered in a sandboxed iframe.

Add a ui field to your plugin.json:

{
"id": "dashboard",
"name": "Pomodoro Dashboard",
"version": "1.0.0",
"author": "Your Name",
"description": "Visual dashboard for tracking pomodoro sessions",
"main": "main.js",
"permissions": ["ui_components", "read_vault"],
"enabled": true,
"ui": {
"page": "index.html",
"tab_name": "Pomodoros",
"icon": "🍅"
}
}

ui fields:

FieldTypeDescription
pagestringPath to the HTML file (relative to plugin folder)
tab_namestringLabel shown on the tab button
iconstringEmoji or short text for the tab icon

When the plugin is enabled, Atlas creates a tab button in the tab bar. Clicking it shows your HTML page in a sandboxed iframe.

Tab plugin HTML runs inside an <iframe sandbox="allow-scripts allow-forms">. This means:

  • Your HTML can run JavaScript and submit forms
  • Your HTML cannot access the parent Atlas window or DOM
  • Your HTML cannot navigate the parent frame
  • Your HTML cannot open popups

Atlas injects a bridge script into your iframe that creates the same atlas global object available to script plugins. All atlas.* API calls work from inside the iframe via postMessage IPC — they’re async and behave identically to the script API.

Create index.html in your plugin folder:

<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, sans-serif;
padding: 1rem;
background: var(--bg-color, #1a1a2e);
color: var(--text-color, #e0e0e0);
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Pomodoro Dashboard</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="total">0</div>
<div>Total Pomodoros</div>
</div>
<div class="stat-card">
<div class="stat-value" id="today">0</div>
<div>Today</div>
</div>
<div class="stat-card">
<div class="stat-value" id="streak">0</div>
<div>Day Streak</div>
</div>
</div>
<script>
// The atlas API is available globally in the iframe
async function loadStats() {
try {
const history = await atlas.data.read('pomodoro-history.json');
const data = JSON.parse(history);
document.getElementById('total').textContent = data.totalPomodoros || 0;
document.getElementById('today').textContent = data.todayCount || 0;
document.getElementById('streak').textContent = data.streak || 0;
} catch (e) {
atlas.plugin.log('No history file yet');
}
}
loadStats();
</script>
</body>
</html>

A plugin can have both a main.js (for toolbar buttons, commands, background logic) and a ui tab (for a rich HTML interface). The main.js runs in the main Atlas context, while the tab HTML runs in its iframe. They share the same atlas.data.* storage, so you can pass data between them:

// In main.js — save state when a pomodoro completes
await atlas.data.write('pomodoro-history.json', JSON.stringify(stats));
// In index.html — read the same data for display
const history = await atlas.data.read('pomodoro-history.json');

Atlas runs a local HTTP server (default port 21847) that external tools can use to interact with the vault. This is useful for integrations with scripts, CLI tools, editors, or other applications outside Atlas.

The plugin server must be started in Atlas Settings > Plugins. Once running, it listens on 127.0.0.1:21847 (localhost only — not accessible from other machines).

All requests require the X-Atlas-Secret header. You can find (or regenerate) the secret in Settings > Plugins.

Check if the server is running.

Terminal window
curl http://localhost:21847/api/health \
-H "X-Atlas-Secret: your-secret"

Response:

{ "status": "ok" }

Read a vault file.

ParameterTypeDescription
pathquery stringPath relative to vault root
Terminal window
curl "http://localhost:21847/api/vault/read?path=daily/2026-02-27.md" \
-H "X-Atlas-Secret: your-secret"

Response:

{ "content": "# February 27, 2026\n\n- [ ] Review PRs\n" }

Create or overwrite a vault file.

Request body:

{ "path": "notes/from-script.md", "content": "# Created by external tool\n" }
Terminal window
curl -X POST http://localhost:21847/api/vault/write \
-H "X-Atlas-Secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"path": "notes/from-script.md", "content": "# Created by script\n"}'

List files and directories.

ParameterTypeDescription
pathquery stringDirectory path relative to vault root
Terminal window
curl "http://localhost:21847/api/vault/list?path=daily" \
-H "X-Atlas-Secret: your-secret"

Delete a vault file.

ParameterTypeDescription
pathquery stringPath relative to vault root
Terminal window
curl -X DELETE "http://localhost:21847/api/vault/delete?path=notes/old.md" \
-H "X-Atlas-Secret: your-secret"

Search the vault using RAG.

Request body:

{ "query": "meeting notes from last week", "limit": 10 }
Terminal window
curl -X POST http://localhost:21847/api/search \
-H "X-Atlas-Secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"query": "project deadlines"}'

Get the current memory context (useful for RAG-aware integrations).

Terminal window
curl http://localhost:21847/api/memory/context \
-H "X-Atlas-Secret: your-secret"
import requests
SECRET = "your-atlas-secret"
BASE = "http://localhost:21847"
HEADERS = {"X-Atlas-Secret": SECRET}
# Read today's daily note
resp = requests.get(
f"{BASE}/api/vault/read",
params={"path": "daily/2026-02-27.md"},
headers=HEADERS
)
content = resp.json()["content"]
# Search the vault
resp = requests.post(
f"{BASE}/api/search",
json={"query": "project status updates"},
headers=HEADERS
)
for result in resp.json()["results"]:
print(result["path"])
# Write a file
requests.post(
f"{BASE}/api/vault/write",
json={"path": "notes/automated-report.md", "content": "# Daily Report\n..."},
headers=HEADERS
)

Every plugin gets a sandboxed data/ directory inside its plugin folder for persistent storage. This is available to all plugins without any permission.

atlas/plugins/pomodoro/
├── plugin.json
├── main.js
└── data/ # Auto-created on first write
├── history.json # Your plugin's data files
└── settings-cache.json

Use atlas.data.read(filename) and atlas.data.write(filename, content) to access it. See the Plugin API Reference for full details.

When to use atlas.data.* vs atlas.config.*:

UseFor
atlas.data.*Larger data: session history, cached content, logs, exported data
atlas.config.getPluginSettings()Small settings: user preferences, toggles, numeric values

Plugin settings (via atlas.config.*) are stored in Atlas’s main config file. Plugin data files are stored in the plugin’s own folder within the vault.


If your plugin has user-configurable options, use atlas.config.getPluginSettings() and setPluginSettings(). These require no special permission and persist across app restarts.

async function onLoad() {
// Load saved settings (or defaults)
const settings = await atlas.config.getPluginSettings();
const workMinutes = settings.workMinutes || 25;
const breakMinutes = settings.breakMinutes || 5;
atlas.plugin.log(`Work: ${workMinutes}min, Break: ${breakMinutes}min`);
}

To let users change settings, you could show a modal:

async function openSettings() {
const current = await atlas.config.getPluginSettings();
const result = await atlas.ui.showModal({
title: 'Pomodoro Settings',
content: `
<div style="padding: 0.5rem;">
<label>Work duration (minutes):</label>
<input type="number" id="work-min" value="${current.workMinutes || 25}" />
<br/><br/>
<label>Break duration (minutes):</label>
<input type="number" id="break-min" value="${current.breakMinutes || 5}" />
</div>
`,
buttons: [
{ label: 'Cancel', value: 'cancel', type: 'secondary' },
{ label: 'Save', value: 'save', type: 'primary' }
]
});
if (result.value === 'save') {
await atlas.config.setPluginSettings({
workMinutes: parseInt(result.formData['work-min']) || 25,
breakMinutes: parseInt(result.formData['break-min']) || 5
});
atlas.ui.showNotification('Settings saved!', 'success');
}
}

Once your plugin is ready to share with other Atlas users:

  1. Create a plugin listing through the Atlas developer portal at atlasnotes.io
  2. Fill in the name, description, category, screenshots, and repository URL
  3. Submit for review — the Atlas team reviews all submissions for safety and quality
  4. Once approved, your plugin is published and visible to all users in the marketplace

The review checks for:

  • Permissions match what the plugin actually uses (don’t request write_vault if you only read)
  • No malicious behavior or hidden data collection
  • A working plugin.json with accurate metadata
  • A public repository or download URL
  • Proper cleanup in onDisable() (no orphaned timers or listeners)

Increment the version field in plugin.json and submit an update through the developer portal. Updates go through a lighter review than the initial submission.


Understanding what plugins can and cannot do helps you build safely and helps users trust your plugin.

  • Read and write vault files (with read_vault / write_vault permission)
  • Add UI elements: toolbar buttons, status bar items, modals, notifications
  • Execute Atlas agent tools (search, create notes, manage tasks)
  • Register keyboard shortcuts
  • Store persistent data in the plugin’s data/ directory
  • Read and write plugin-specific settings
  • Make HTTP requests (with network permission)
  • Access the filesystem outside the vault
  • Execute shell commands or spawn processes
  • Access the Atlas app’s internal state or DOM directly (tab plugins run in sandboxed iframes)
  • Access other plugins’ data directories
  • Call APIs that require permissions not listed in their manifest

Script plugins (main.js) run inside a Function() sandbox in the Atlas webview. They can only access the atlas global — not window, document, or other browser APIs directly.

Tab plugins (index.html) run inside <iframe sandbox="allow-scripts allow-forms">. They cannot access the parent frame’s DOM or JavaScript context. All Atlas API calls go through a secure postMessage bridge.

All vault operations reject path traversal (..). Plugin data filenames reject path separators (/, \) and traversal. These checks are enforced at both the JavaScript API layer and the Rust backend.