Plugins
Build a plugin
A plugin is a folder — a manifest, an HTML entry, and whatever assets you need. Here's everything to go from empty folder to an installable .zip.
Anatomy of a bundle
There's no build step and no required framework — a plugin is just files, zipped:
my-plugin/ ├─ atomcut.plugin.json # manifest — identity, permissions, window ├─ ui.html # your plugin's UI — the sandbox entry point ├─ main.js # optional — code can also be inline in ui.html └─ icon.svg # optional — shown in the plugin list
atomcut.plugin.json must sit at the bundle root. ui.html (or whatever you name mainin the manifest) is loaded into a sandboxed iframe as the plugin's UI — any local <script src>, stylesheet or media it references is inlined automatically, so the whole thing runs from a single, offline document with no network access.
The manifest
A minimal, real manifest:
{
"manifestVersion": 1,
"id": "com.acme.confetti",
"name": "Confetti",
"version": "1.0.0",
"description": "Rain text confetti onto the canvas.",
"author": { "name": "Acme", "url": "https://acme.dev" },
"api": 1,
"main": "ui.html",
"icon": "icon.svg",
"capabilities": ["document:write", "selection:read", "ui:notify"],
"window": { "width": 360, "height": 560, "resizable": true },
"categories": ["Utility"]
}id is reverse-DNS style — com.yourname.plugin-name— the same idea as a bundle identifier. It's the uniqueness key: whoever installs your plugin, reinstalling a .zip with the same id replaces the old copy instead of adding a second one, which is also how you ship updates. Pick it once and keep it stable.
Every field, its type, and its default is in the manifest & API reference.
A minimal ui.html
Inside the sandbox, window.atomcut is ready to use as soon as your script runs — no import, no setup:
<!doctype html>
<html>
<head><meta charset="utf-8" /></head>
<body>
<button id="go">Add title</button>
<script>
const { atomcut } = window;
document.getElementById("go").onclick = async () => {
const info = await atomcut.document.getInfo();
await atomcut.document.insertText({ text: info.name, fontSize: 120 });
await atomcut.ui.notify("Added a title layer", "success");
};
atomcut.on("selectionchange", (sel) => {
console.log("selection", sel);
});
</script>
</body>
</html>Pair that with the manifest above (save it as ui.html next to atomcut.plugin.json) and you have a complete, installable plugin. Every method on window.atomcut is async — it's a postMessageround trip to the editor — and rejects if you didn't declare the capability it needs. See the full method list in the API reference.
The developer workflow
While you're iterating, skip the zip-and-reinstall loop entirely. Open Plugins → Manage plugins…, then Connect folderand pick your plugin's directory. AtomCut installs it right away and keeps watching the folder — save a file, and it's silently reinstalled; any open window picks up the change on the spot. It's the same live-reload feel as a design tool, without a bundler in the loop.
Connect folderuses the browser's File System Access API, so it's Chromium-only (Chrome, Edge, Arc, and similar) — it won't appear as an option in Safari or Firefox. The folder handle also only lives for the session: refresh the page and you'll need to reconnect it, which re-grants permission cleanly with no stale state to clean up.
Once it works the way you want, zip the folder exactly as it is — atomcut.plugin.json at the root — and share the .zip. Anyone installs it with Install from file; no browser requirement, no dev tools, same plugin.
Capabilities — ask for the minimum
Every call your plugin makes is checked against the capabilities its manifest declared; a call for something you didn't ask for is rejected before it touches the document. There are four today:
document:read— read the document's frames, layers and properties.document:write— create, edit and remove layers.selection:read— see and react to the current selection.ui:notify— show toast messages in the editor.
Declare only what you actually call. It keeps your plugin's intent honest and legible to anyone deciding whether to install it — the full table, with what each one grants, is on the reference page.
Protocol compatibility
The manifest's apifield is the plugin protocol major you're building against. If it's newer than what the running editor speaks, installation is refused up front with a message asking for a newer AtomCut — rather than a plugin silently failing at runtime.
Browsing the source is often faster than reading docs — the manifest schema and the full window.atomcut types live in packages/plugin-sdk in the AtomCut repo.