docs/plugin-development.md
This is a comprehensive documentation of the Super Productivity Plugin System. This guide covers everything you need to know about creating plugins for Super Productivity.
These docs might not always be perfectly up to date. You find the latest typescript interfaces here: types.ts
Personally I think the best way to figure out how to write a plugin is to check out the example plugins:
If you want to build a sophisticated UI there is a boilerplate available for solidjs: boilerplate-solid-js
my-plugin/
├── manifest.json # Plugin metadata (required)
├── plugin.js # Main plugin code that is launched when activated and when Super Productivity starts
├── index.html # UI interface (optional) => requires iFrame:true in manifest
└── icon.svg # Plugin icon (optional)
manifest.json:
{
"id": "hello-world",
"name": "Hello World Plugin",
"version": "1.0.0",
"description": "My first Super Productivity plugin",
"manifestVersion": 1,
"minSupVersion": "14.0.0"
}
plugin.js:
console.log('Hello World plugin loaded!');
// Show a notification
PluginAPI.showSnack({
msg: 'Hello from my plugin!',
type: 'SUCCESS',
});
// Demo a simple counter
await PluginAPI.setCounter('hello-count', 0);
PluginAPI.registerHeaderButton({
label: 'Hello (Count: 0)',
icon: 'waving_hand',
onClick: async () => {
const newCount = await PluginAPI.incrementCounter('hello-count');
PluginAPI.showSnack({
msg: `Button clicked! Count: ${newCount}`,
type: 'INFO',
});
},
});
The manifest.json file is required for all plugins and defines the plugin's metadata and configuration.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✓ | Unique identifier for your plugin (use kebab-case) |
name | string | ✓ | Display name shown to users |
version | string | ✓ | Semantic version (e.g., "1.0.0") |
description | string | ✓ | Brief description of what your plugin does |
manifestVersion | number | ✓ | Currently must be 1 |
minSupVersion | string | ✓ | Minimum Super Productivity version required |
author | string | Plugin author name | |
homepage | string | Plugin website or repository URL | |
icon | string | Path to icon file (SVG recommended) | |
iFrame | boolean | Whether plugin uses iframe UI (default: false) | |
sidePanel | boolean | Show plugin in side panel (default: false), requires iFrame:true | |
permissions | string[] | The permissions the plugin needs (e.g., ["nodeExecution"]) | |
hooks | string[] | App events to listen to | |
uiKit | boolean | Enable UI Kit CSS reset for iframe plugins (default: true). Set to false to disable. |
{
"id": "my-advanced-plugin",
"name": "My Advanced Plugin",
"version": "2.1.0",
"description": "An advanced plugin with UI and hooks",
"manifestVersion": 1,
"minSupVersion": "14.0.2",
"author": "John Doe",
"homepage": "https://github.com/johndoe/my-plugin",
"icon": "icon.svg",
"iFrame": true,
"sidePanel": false,
"permissions": ["nodeExecution"],
"hooks": ["taskComplete", "taskUpdate", "currentTaskChange"]
}
plugin.js)Pure JavaScript plugins that run in a sandboxed environment with full API access.
Use when:
Example:
// Register multiple UI elements
PluginAPI.registerHeaderButton({
label: 'My Button',
icon: 'star',
onClick: async () => {
const tasks = await PluginAPI.getTasks();
console.log(`You have ${tasks.length} tasks`);
},
});
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskId) => {
console.log(`Task ${taskId} completed!`);
});
index.html)Plugins that render custom UI in a sandboxed iframe.
Use when:
Important: When using iframes, you must inline all CSS and JavaScript directly in the HTML file. External stylesheets and scripts are blocked for security reasons.
Example index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>My Plugin UI</title>
<!-- CSS must be inlined. Theme variables and UI Kit are injected automatically. -->
<style>
body {
padding: var(--s3);
}
.task-list {
background: var(--card-bg);
border-radius: var(--card-border-radius);
padding: var(--s2);
box-shadow: var(--whiteframe-shadow-2dp);
}
.task-item {
padding: var(--s);
border-bottom: 1px solid var(--divider-color);
}
</style>
</head>
<body>
<h1>My Plugin</h1>
<div id="content">
<button id="loadTasks">Load Tasks</button>
<div
id="taskList"
class="task-list"
></div>
</div>
<!-- JavaScript must be inlined -->
<script>
document.getElementById('loadTasks').addEventListener('click', async () => {
try {
const tasks = await PluginAPI.getTasks();
const taskList = document.getElementById('taskList');
taskList.innerHTML = '<h3>Your Tasks:</h3>';
tasks.forEach((task) => {
const taskEl = document.createElement('div');
taskEl.className = 'task-item';
taskEl.textContent = task.title;
taskList.appendChild(taskEl);
});
PluginAPI.showSnack({
msg: `Loaded ${tasks.length} tasks`,
type: 'SUCCESS',
});
} catch (error) {
console.error('Error loading tasks:', error);
PluginAPI.showSnack({
msg: 'Failed to load tasks',
type: 'ERROR',
});
}
});
</script>
</body>
</html>
Iframe plugins automatically receive:
CSS variables — All theme variables (colors, spacing, shadows, transitions) are injected as CSS custom properties on :root. Use var(--c-primary), var(--bg), var(--text-color), etc.
UI Kit CSS reset — By default, basic HTML elements (button, input, select, textarea, table, a, h1–h6, p, code, pre, hr, etc.) are styled to match the app's look. This is injected before your plugin's own styles, so your CSS always wins.
To disable the UI Kit, add "uiKit": false to your manifest.
Button variants:
<button> — Neutral card-background button with border<button class="btn-primary"> — Filled primary-color button (white text)<button class="btn-outline"> — Transparent button with primary-color border and text, fills on hoverCard component:
<div class="card"> — Card with background, shadow, rounded corners, and border<div class="card card-clickable"> — Adds hover lift effect and primary border highlightUtility classes:
.text-muted — Muted text color (var(--text-color-muted)).text-primary — Primary theme color (var(--c-primary)).page-fade — Fade-in animation (0.3s ease)Key CSS variables:
--bg, --bg-darker — Background colors--text-color, --text-color-muted — Text colors--c-primary, --c-accent, --c-warn — Theme colors--card-bg, --card-shadow, --card-border-radius — Card styling--divider-color — Border/divider color--s, --s2, --s3, --s4, --s-half, --s-quarter — Spacing scale--transition-standard — Standard transition--font-primary-stack — App font stack--whiteframe-shadow-1dp through --whiteframe-shadow-24dp — Elevation shadows--is-dark-theme — 1 if dark theme, 0 if lightgetTasks() - Get all active tasksgetArchivedTasks() - Get archived tasksgetCurrentContextTasks() - Get tasks in current contextaddTask(task) - Create a new taskupdateTask(taskId, updates) - Update existing taskgetAllProjects() - Get all projectsaddProject(project) - Create new projectupdateProject(projectId, updates) - Update projectgetAllTags() - Get all tagsaddTag(tag) - Create new tagupdateTag(tagId, updates) - Update tagSimple counters let you track lightweight metrics (e.g., daily clicks or habits) that persist and sync with your data. There are two levels: basic (key-value pairs for today's count) and full model (full CRUD on SimpleCounter entities with date-specific values).
These treat counters as a simple { [id: string]: number } map for today's values (auto-upserts via NgRx).
| Method | Description | Example |
|---|---|---|
getAllCounters() | Get all counters as { [id: string]: number } | const counters = await PluginAPI.getAllCounters(); console.log(counters['my-key']); |
getCounter(id) | Get today's value for a counter (returns null if unset) | const val = await PluginAPI.getCounter('daily-commits'); |
setCounter(id, value) | Set today's value (non-negative number; validates id regex /^[A-Za-z0-9_-]+$/) | await PluginAPI.setCounter('daily-commits', 5); |
incrementCounter(id, incrementBy = 1) | Increment and return new value (floors at 0) | const newVal = await PluginAPI.incrementCounter('daily-commits', 2); |
decrementCounter(id, decrementBy = 1) | Decrement and return new value (floors at 0) | const newVal = await PluginAPI.decrementCounter('daily-commits'); |
deleteCounter(id) | Delete the counter | await PluginAPI.deleteCounter('daily-commits'); |
Example:
// Track daily commits
let commits = (await PluginAPI.getCounter('daily-commits')) ?? 0;
await PluginAPI.incrementCounter('daily-commits');
PluginAPI.showSnack({
msg: `Commits today: ${await PluginAPI.getCounter('daily-commits')}`,
type: 'INFO',
});
For advanced use: Full CRUD on counters with metadata (title, enabled state, date-specific values via countOnDay: { [date: string]: number }).
| Method | Description | Example |
|---|---|---|
getAllSimpleCounters() | Get all as SimpleCounter[] | const all = await PluginAPI.getAllSimpleCounters(); |
getSimpleCounter(id) | Get one by id (returns undefined if not found) | const counter = await PluginAPI.getSimpleCounter('my-id'); |
updateSimpleCounter(id, updates) | Partial update (e.g., { title: 'New Title', countOnDay: { '2025-11-17': 10 } }) | await PluginAPI.updateSimpleCounter('my-id', { isEnabled: false }); |
toggleSimpleCounter(id) | Toggle isOn state (throws if not found) | await PluginAPI.toggleSimpleCounter('my-id'); |
setSimpleCounterEnabled(id, isEnabled) | Set enabled state | await PluginAPI.setSimpleCounterEnabled('my-id', true); |
deleteSimpleCounter(id) | Delete by id | await PluginAPI.deleteSimpleCounter('my-id'); |
setSimpleCounterToday(id, value) | Set today's value (YYYY-MM-DD) | await PluginAPI.setSimpleCounterToday('my-id', 10); |
setSimpleCounterDate(id, date, value) | Set value for specific date (validates YYYY-MM-DD) | await PluginAPI.setSimpleCounterDate('my-id', '2025-11-16', 5); |
Example:
// Create/update a habit counter
await PluginAPI.updateSimpleCounter('habit-streak', {
title: 'Daily Streak',
type: 'ClickCounter',
isEnabled: true,
countOnDay: { '2025-11-17': 1 }, // Today's count
});
await PluginAPI.toggleSimpleCounter('habit-streak');
const counter = await PluginAPI.getSimpleCounter('habit-streak');
console.log(`Streak on: ${counter.isOn}`);
// Show snackbar notification
PluginAPI.showSnack({
msg: 'Operation completed!',
type: 'SUCCESS', // SUCCESS, ERROR, INFO, WARNING
ico: 'check', // Optional Material icon
actionStr: 'Undo', // Optional action button
actionFn: () => console.log('Undo clicked'),
});
// System notification
PluginAPI.notify({
title: 'Task Complete',
body: 'Great job!',
ico: 'done',
});
// Open a dialog
const result = await PluginAPI.openDialog({
title: 'Confirm Action',
content: 'Are you sure?',
okBtnLabel: 'Yes',
cancelBtnLabel: 'No',
});
PluginAPI.registerHeaderButton({
id: 'my-header-btn', // Optional unique ID
label: 'Click Me',
icon: 'star', // Material icon name
onClick: () => {
console.log('Header button clicked');
},
});
PluginAPI.registerMenuEntry({
label: 'My Plugin Action',
icon: 'extension',
onClick: () => {
console.log('Menu item clicked');
},
});
PluginAPI.registerSidePanelButton({
label: 'My Panel',
icon: 'dashboard',
onClick: () => {
PluginAPI.showIndexHtmlAsView();
},
});
PluginAPI.registerShortcut({
keys: 'ctrl+shift+p',
label: 'My Plugin Shortcut',
action: () => {
console.log('Shortcut triggered');
},
});
// Available hooks
const hooks = {
TASK_COMPLETE: 'taskComplete',
TASK_UPDATE: 'taskUpdate',
TASK_DELETE: 'taskDelete',
CURRENT_TASK_CHANGE: 'currentTaskChange',
FINISH_DAY: 'finishDay',
LANGUAGE_CHANGE: 'languageChange',
PERSISTED_DATA_UPDATE: 'persistedDataUpdate',
ACTION: 'action',
};
// Register hook listener
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskId) => {
console.log(`Task ${taskId} completed!`);
});
// Listen to Redux actions
PluginAPI.registerHook(PluginAPI.Hooks.ACTION, (action) => {
if (action.type === 'ADD_TASK_SUCCESS') {
console.log('New task added:', action.payload);
// Bonus: Increment a counter on task add
PluginAPI.incrementCounter('tasks-added-today');
}
});
You can persist data that will also be synced vai the persistDataSynced and loadSyncedData APIs. For local storage I recommend using localStorage.
// Save plugin data
await PluginAPI.persistDataSynced(JSON.stringify({ count: 42 }));
// Load saved data
const data = await PluginAPI.loadSyncedData();
console.log(data); // '{ count: 42 }'
console.logs should be kept to a minimum.
<!-- Good: Everything inlined -->
<!DOCTYPE html>
<html>
<head>
<style>
/* All styles here */
</style>
</head>
<body>
<div id="app"></div>
<script>
// All JavaScript here
</script>
</body>
</html>
In iframe context, these methods are NOT available:
registerHeaderButton()registerMenuEntry()registerSidePanelButton()registerShortcut()registerHook()execNodeScript()// Add debug logging
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[MyPlugin]', ...args);
}
}
// Test API methods
async function testAPI() {
log('Testing getTasks...');
const tasks = await PluginAPI.getTasks();
log('Tasks:', tasks);
log('Testing showSnack...');
PluginAPI.showSnack({
msg: 'API test successful!',
type: 'SUCCESS',
});
}
Plugin not loading:
API methods failing:
Iframe not displaying:
If you create a useful plugin, consider:
Happy plugin development! 🚀
Ctrl+Shift+i opens the console) and iterate until it works. Don't expect that everything works on your first try.Can you you write me a plugin for Super Productivity that plays a beep sound every time i click on a header button (You need to add a header button via PluginAPI.registerHeaderButton).
Here are the docs: https://github.com/super-productivity/super-productivity/blob/master/docs/plugin-development.md
Don't use any PluginAPI methods that are not listed in the guide.
Please give me the output as flat zip file to download.