Skip to content

Plugin API

A plugin is a manifest plus one HTML document. The editor opens the document in a floating panel, and the document reads and changes the project only through the functions on this page.

ReferenceAdvancedAbout 15 minWebDesktop

Where a plugin runs

A plugin is a single .sshowplugin file: a renamed zip file that holds the manifest (plugin.json) and one UI document. An icon is optional. The editor uses only these files. The 13 plugins use the same format.

When you click a plugin in the list, the editor opens a separate floating panel next to the list and loads the UI document in a sandboxed frame inside it. The SSHOWPlugin SDK and the editor’s theme styles go in before the document’s scripts, so your script can call SSHOWPlugin.connect() from the start.

A plugin reaches the editor only through the functions in the tables below. Every function returns a Promise, and applyActions is the only way to change the document. The AI Assistant and AI apps (Connect an AI app (MCP)) change the document with the same actions.

What the handle returned by SSHOWPlugin.connect() holds
Name on the handleWhat it holds
apiVersionThis editor’s plugin API version. Currently 1.
engineVersionThe editor engine’s version. Use it for display only.
documentReading the document, selection, switching scenes, and writing. Reading and selection · Writing: applyActions
assetsReading and registering the bytes of project files. Assets
storageKeeping plugin settings. Storage
eventsNotifications when the document, selection, or clock changes. Events
uiPanel size and theme. Panel UI and keyboard

Write your first plugin

All you need is a text editor and a zip tool. You build a Hello plugin that adds one text object to the canvas when you click a button.

  1. Write plugin.json

    Create an empty folder, and create plugin.json inside it. Copy the first row of the table below exactly. The rules for each field are in Manifest.

  2. Write ui.html

    Create ui.html in the same folder. Add one button element whose id is insert. Below it, add a script element, and write Script 1 and Script 2 from the table below, in that order. A plugin can’t load other files, so all styles and scripts go in this file.

What to write in the Hello plugin’s two files
FileContents
plugin.json{ "id": "com.example.hello", "name": "Hello", "version": "1.0.0", "api": 1, "main": "ui.html", "author": "Example" }
ui.html · UIOne button element, with the id insert and the text Insert. Put the script element below this button. You can leave out the html, head, and body elements.
ui.html · Script 1const action = { op: 'create_object', type: 'text', config: { name: 'Hello', data: { text: 'Hello, SSHOW', fontSize: 48, autoSize: true }, transform: { x: 200, y: 200 } } };
ui.html · Script 2(async () => { const api = await SSHOWPlugin.connect(); document.querySelector('#insert').onclick = () => api.document.applyActions([action], 'Hello'); })();

Import and run it

  1. Pack it as .sshowplugin

    Open the folder, select plugin.json and ui.html, and compress them into a zip. Rename the file to hello.sshowplugin. When you import, you can choose only files that end in .sshowplugin. On Windows with file extensions hidden, check that .zip isn’t left at the end.

  2. Import it into the editor

    At the top right of the editor, click the light-bulb-shaped Plugins button. In the panel’s title row, click the plus-shaped Import plugin (.sshowplugin) button, and choose hello.sshowplugin. Hello appears at the bottom of the list. If you don’t see it, scroll to the end of the list.

    Choosing hello.sshowplugin with the + button and scrolling to the end of the list shows Hello at the bottom. Video: Clicking the light-bulb Plugins button opens the panel, and hello.sshowplugin is chosen with the + button in its title row. Scrolling to the end of the list shows Hello at the bottom.
  3. Run it

    Click Hello in the list. A floating panel opens next to the list and shows ui.html. Click Insert, and “Hello, SSHOW” appears on the canvas.

    Clicking Hello opens the panel, and clicking Insert adds “Hello, SSHOW” to the canvas. Video: Clicking Hello in the plugin list opens the plugin UI in a floating panel, and clicking its Insert button adds the text Hello, SSHOW to the canvas.

Both files must be at the top level of the zip

The editor looks for the exact name plugin.json inside the zip. If you compress the whole folder, the name becomes “hello/plugin.json”, and you get “This file could not be read as a .sshowplugin package.” Open the folder, select the files inside, and compress those.

Manifest (plugin.json)

Five fields are required, and three are optional. Unknown fields are ignored, so a manifest with newly added fields still opens in older editors.

In the title row of a running plugin’s panel, click the Plugin info button (an i in a circle) to see the version, author, ID, and description.

Manifest fields and rules
FieldRequiredRules
idRequiredLowercase reverse-domain format (com.example.hello). Use only a–z, 0–9, periods, and hyphens, and start with a lowercase letter or a digit. Two periods can’t appear in a row. When publishing, it can be up to 100 characters, and the account that first submits it owns it from then on.
nameRequiredThe name shown in the list and in the panel title. Up to 100 characters when publishing.
versionRequiredx.y.z, three numbers joined by periods (1.0.0).
apiRequiredThe plugin API version. Currently 1. If it differs from the editor’s, the plugin isn’t imported.
mainRequiredThe file name of the UI document (ui.html).
descriptionOptionalThe description shown in the list and in Plugin info. Up to 2000 characters when publishing.
authorOptionalThe author shown in the list and in Plugin info. Up to 100 characters when publishing. In the published list, the author is the submitting account, not this value.
iconOptionalThe name of the icon file in the package. Use png, svg, jpg, jpeg, or webp.

Reading and selection

Every read function returns a copy as of that moment. Changing the copy doesn’t change the document. To make a change, send it with applyActions.

For images, video, and audio, data.src comes as an asset:// URL for a project asset, and you read the bytes with assets.get. A long src that isn’t an asset (such as a data: URL) comes as a placeholder that records only its length (src len=… kind=…), so don’t send it back. Web requests in interactions come with the values in their URLs and their header values masked.

Read and selection functions
FunctionReturns · does
document.getState(){ canvas, activeSceneId, scenes, fonts }: the canvas size, the id of the current scene, the list of scenes, and the names of the fonts in the document. The current scene includes all of its objects and the scene’s motion and interaction. Other scenes include only each object’s id, type, and name.
document.getObject(id)A copy of one object in the current scene. null if the object is in another scene or doesn’t exist. To read one from another scene, open that scene with setActiveScene first.
document.getSelection()An array of copies of the currently selected objects.
document.getTextLayout(id){ char, word, line } for a text object. Each is an array of { text, line, word, localX, localY, width, height }. It uses the same measurement as what’s drawn on screen, so letter spacing and line breaks match, and the boxes are in the object’s own coordinates before transforms. text holds the characters with variable values filled in, and line and word are numbers in reading order. null if the text isn’t in the current scene, and an empty array if it hasn’t been drawn yet.
document.getTimelineTime()The editor’s animation clock, in ms. In Animation mode it’s the playhead position, and in Design mode it’s 0. Start timeline writes from this value, and read it again right before you write.
document.setSelection(ids)Selects objects of the current scene in the editor. Use it to hand the objects you just created back to the user, already selected. Ids that don’t exist are silently dropped, and nothing is added to the undo history.
document.setActiveScene(sceneId)Switches the current scene. Unknown ids are rejected, so you never keep writing to the wrong scene.

Writing: applyActions

document.applyActions(actions, label) is the only way to change the document. It applies the actions in the array in the order written, and the whole call becomes one undo step (one ⌘Z). label is that step’s name, and if you leave it empty, the plugin’s name is used.

It returns { applied, skipped, ids }. applied is the number of actions applied. skipped holds { index, op, id, code, reason } for each skipped action. Even if some actions are invalid, the rest are still applied.

In a create action, config.id is an alias for this call only. Later actions in the same array can refer to that object by the alias. The engine sets the real id, which comes back paired with the alias in ids.objects, ids.scenes, or ids.variables. From the next call on, use the real id. Name aliases like 'bar'. If you use the engine’s id pattern, like o1 or s2, and the engine happens to create that same id, the alias is left out of ids.

Writes take the same path as edits you make by hand in the editor. In Animation mode, when Auto-keyframe is on, values such as position, size, and opacity are recorded as keyframes at the playhead. To edit keyframes, see Work with keyframes.

Three bars and three labels added in one call all disappear with a single ⌘Z. Video: Clicking Insert chart in the Chart plugin adds three bars and three labels at once, and pressing ⌘Z once removes all six together.

The 25 actions

The ops you can put in applyActions, and their fields
opFields (+ optional)What it does
create_objecttype, config (+ sceneId, options)Creates an object. type is rect, circle, path, text, image, video, audio, group, frame, or table. Give options.parentObjectId to create it inside a group or frame, and use options.index to set its place in the list.
update_objectid, set (+ sceneId)Changes only the keys in set. set keys and how they merge
delete_objectid (+ sceneId)Deletes an object. Deleting the text in a table cell keeps the cell and only empties its text.
duplicate_objectid (+ sceneId, options)Duplicates an object.
move_objectid (+ sceneId, options)options.parentObjectId puts it inside another group or frame, and options.index sets its place there. options.to changes the stacking order with front, back, forward, or backward.
group_objectsids (+ config, sceneId)Groups two or more objects. You can include aliases created in the same call.
ungroupid (+ sceneId)Ungroups a group.
convert_to_pathid (+ sceneId)Converts an object to a path.
insert_table_rowsid (+ index, count, values, sceneId)Inserts rows into a table (Tables). Without index, they go after the last row. values sets the text of the new cells.
insert_table_columnsid (+ index, count, values, sceneId)Inserts columns into a table.
delete_table_rowsid, index (+ count, sceneId)Deletes rows. count defaults to 1.
delete_table_columnsid, index (+ count, sceneId)Deletes columns.
merge_table_cellsid, range (+ sceneId)Merges the cells in range { rowIndex, colIndex, rowSpan, colSpan }. The range must cover two or more cells, and all of them must be text cells.
unmerge_table_cellsid, range (+ sceneId)Splits the merged cells within range.
create_sceneconfig (+ options)Creates an empty scene, and the editor switches to it. To add objects to it, give this scene’s id or alias as sceneId in create_object.
update_sceneset (+ sceneId)Changes a scene’s keys.
delete_scene(+ sceneId)Deletes a scene.
duplicate_scene(+ sceneId)Duplicates a scene, and the editor switches to the copy.
move_scenenewIndex (+ sceneId)Changes a scene’s place in the order.
set_scene_sizesize: { width, height }Changes the canvas size. A document has only one size, so it applies to every scene.
create_variableconfig (+ options)Creates a variable (Variables).
update_variablevariableId, setChanges a variable’s keys.
delete_variablevariableIdDeletes a variable.
move_variablevariableId, newIndexChanges a variable’s place in the order.
set_documentsetChanges the document’s name, description, and notes.

set keys and how they merge

If set contains even one unknown key, the whole action is skipped, and the reason goes into skipped. For any action without sceneId, the target is the scene that’s current when you call. After create_scene or duplicate_scene, the current scene switches to the new one, so give sceneId to write to the original scene in the next call.

Keys you can use in set, by action
setAllowed keys
update_objectname · description · size · transform · distort · layout · style · opacity · blendMode · locked · visible · motion · interaction · data
update_scenename · description · notes · style · visible · motion · interaction · data · clip
update_variablename · description · type · value
set_documentname · description · notes
How each set key merges with the current value
KeyHow it merges
transform · size · data · layoutOnly the subkeys you send change, and the rest stay as they are.
style · distortReplaced as a whole. When you send style, always send all of fills, strokes, and effects.
motionReplaced as a whole, separately for transitions and animations. If you send only animations, transitions stay as they are, and the other way around. To change one keyframe, read that whole group, change it, and send the whole group back.
interactionAll rules are replaced. Read the current rules, change them, and send them back. You can’t write new web request actions. Create them by hand in the Interactions panel, as shown in Requests and variables. Existing requests are kept if you leave them as you read them.

When sent values are changed or rejected

Cases where applyActions changes or rejects the values you send
What you sendHow it’s handled
transform.rotateX · rotateY · rotateZTaken in degrees and stored in radians. The old name rotate is read as rotateZ. Angles in read results and in motion tracks are in radians, so convert an angle you read to degrees before you send it back in set.
A fill with color but no typetype is filled in as 'solid'. Fills and effects that can’t be fixed are discarded.
The two-character sequences \n and \t in data.textBecome real line breaks and tabs.
data.src · src in image fillsOnly asset:// URLs for project assets are accepted. If you give an http(s) URL or a data: URL, that action is skipped. Register files first through Assets.

Example: create, edit, and select

This example puts an action that creates an object and an action that edits it in one call. 'bar' is an alias for this call only, and after the call, ids.objects.bar is the real id.

As in Hello’s Script 2, write the code below inside an async function, where the api you got from connect() is available. Writing await at the top level, outside a function, causes a syntax error.

transform.x and y are the position of the object’s anchor point, and a shape’s anchor point is at its center by default (anchorX and anchorY 0.5). So this bar spans from (340, 180) to (460, 420).

Code that sends two actions in one call and selects the result
StepCode
Build the actionsconst actions = [ { op: 'create_object', type: 'rect', config: { id: 'bar', name: 'bar', size: { width: 120, height: 240 }, transform: { x: 400, y: 300 }, style: { fills: [ { type: 'solid', color: '#8A8A8E' } ], strokes: [], effects: [] } } }, { op: 'update_object', id: 'bar', set: { opacity: 0.8 } } ];
Sendconst { applied, skipped, ids } = await api.document.applyActions(actions, 'Add bar');
Check what was skippedif (skipped.length) console.warn(skipped);
Select and hand overawait api.document.setSelection([ids.objects.bar]);

Assets

You read the bytes of images, video, and audio by their asset:// URLs, and you register new bytes to get a URL. A plugin that edits the selected image first reads the image’s data.src with getSelection. It gets the bytes with assets.get, edits them, registers the result with assets.register, and then changes data.src to the new URL with update_object.

Put the URL you get into an action right away. Assets that aren’t used anywhere may be cleaned up.

Asset functions
FunctionReturns · does
assets.get(uri){ bytes, mimeType, originalName } for an asset:// URL. bytes is an ArrayBuffer. null if there’s no such asset.
assets.register(bytes, { mimeType, originalName })Registers the bytes as a project asset and returns an asset:// URL. bytes must be an ArrayBuffer, not a Uint8Array, and each can be up to 10 MB. The same content gets the same URL.

Storage

Closing the panel also discards the plugin’s document, so values kept in script variables are gone when you open it again. Keep values that must last, like options, in storage. localStorage isn’t available inside the frame, so the editor keeps one record per plugin id on its behalf.

The record lives in the app, not in the document. Every project you open in this app sees the same record, and it isn’t saved in .sshow files. Web, desktop, and tablet keep separate records, and importing the same id again doesn’t erase the record. Put anything that must stay with the document into objects with applyActions.

Storage functions
FunctionReturns · does
storage.get(key)The value for that key. null if it was never saved.
storage.set(key, value)Saves a JSON value. A plugin’s whole record can be up to 64 KB. A save that goes over is rejected, and the record stays as it was. Saving undefined deletes that key.
storage.remove(key)Deletes one key.
storage.keys()An array of the keys this plugin has saved.

Events

Listen with events.on(type, callback), and stop with events.off(type, callback). The callback receives no value. It’s a signal to read again, so when it arrives, read again with the read functions. Types other than the three below are rejected, and all subscriptions end when the plugin stops.

Events a plugin can listen to
typeWhen it arrives
history:updateWhen the document changes. Edits, undo, and redo all count.
ui:modes:edit:changeSelectedObjectsWhen the selection changes.
motion:animation:timeUpdateWhen the animation clock moves. During playback it arrives every frame, so throttle your handling.

Panel UI and keyboard

Request a UI size with ui.resize(size). A single number is the height, and { width, height } sets whichever sides you include. The panel starts 340px wide and 52% of the window height, and it resizes only between 280 × 240px and 90% of the window. A requested size stays, as if the user had resized the panel.

The UI document gets the editor’s design values as the CSS variables below, and they follow the same light or dark setting as the editor. Use them like var(--sshow-primary), and the panel changes along with the theme with no extra code. Scrollbars are the same 6px bars as in the editor, and if you write your own ::-webkit-scrollbar rules, yours take priority.

ui.getTheme() returns { mode, colors, borderRadius, fontSize }. mode is 'light' or 'dark'. Use it only when your script needs color values, such as when drawing on a canvas element.

Editor shortcuts keep working even when the plugin UI has focus. Keys the plugin doesn’t handle go on to the editor: ⌘Z undoes, Tab switches the edit mode, and Space pans the view (a short press plays the timeline in Animation mode). So inside the UI, Tab can’t move focus and Space can’t press buttons.

To use a key in your plugin, stop it before it reaches window. For keys your script handles itself, call preventDefault(). For keys that should keep a browser default, like moving focus or pressing a button, call stopPropagation() in both keydown and keyup. preventDefault() stops that default too. The SDK forwards keys from window, so attach your listeners to document or an element, not to window. While an input, a textarea, or a contenteditable element has focus, no keys are forwarded.

CSS variables in the plugin document
CSS variableValue
--sshow-primaryAccent color #2196F3
--sshow-primary-strongSurface of a button that’s on or pressed. The accent color, slightly darker.
--sshow-primary-softSurface of a button on hover. The accent color, darker and slightly transparent.
--sshow-primary-foregroundText on the accent color #ffffff
--sshow-secondarySecondary color #FFC107
--sshow-foregroundBody text. Light #30353e · Dark #f5f5f5
--sshow-backgroundTranslucent panel surface
--sshow-background-solidOpaque panel surface
--sshow-border-colorBorders
--sshow-radius13px
--sshow-font-size12px. The text size inside editor panels.
--sshow-scrollbar-size6px
--sshow-scrollbar-radius3px

Sandbox and limits

A plugin runs in a sandboxed frame, separate from the editor. The limits below protect the documents and accounts of the people who use plugins.

Plugin limits
ItemLimit
NetworkBlocked. You can’t use fetch, external scripts, external images, or web fonts. Write scripts and styles directly in the UI document, and embed images and fonts as data: URLs. Images can also use blob: URLs.
ScriptsScripts in the document and WebAssembly work. eval and new Function don’t.
FrameThe sandbox allows only allow-scripts. Inside the frame, localStorage and cookies aren’t available, and alert and confirm dialogs and new windows don’t open.
PackageUp to 64 zip entries. plugin.json, the UI document, and the icon can each be up to 5 MB uncompressed, and nothing other than these three is used. When publishing, the whole package can be up to 10 MB, and any entry over 5 MB uncompressed is rejected, even an unused one.
Assets · storageUp to 10 MB per assets.register call, and up to 64 KB of storage per plugin.
RunningOnly one plugin is open at a time. Opening another plugin or closing the panel stops it.
API versionIf api in plugin.json differs from the editor’s API version, the plugin isn’t imported. The version goes up only for changes that would stop existing plugins from working. New functions can be added without a version change, so check that one exists as you use it, like api.storage?.get.
ErrorsA failed call is rejected with an Error, and its message holds the reason.
LanguagePlugins get no value that tells them the editor’s UI language. The 13 built-in plugins also appear only in English.

Edit and check again

There are two ways to check a plugin in the editor again after you change it. You can import the file again anywhere, and in the Windows desktop app you can also have the app watch a folder.

Import again

Web · Desktop

Pack the changed files again, and import the package with Import plugin (.sshowplugin). A plugin with the same id is replaced by the new file, so upgrading, rolling back, and reinstalling are all the same action. Below the list, “Hello updated: 1.0.0 → 1.1.0.” or “Hello 1.0.0 reinstalled.” appears, and a running plugin reopens with the new version. If the file is rejected because it can’t be read, the existing plugin stays. If it’s rejected because the api value differs, the existing one is also removed from the list, so import a fixed file.

Import againImporting the 1.1.0 file shows “Hello updated: 1.0.0 → 1.1.0.” and reopens the panel with the new version. Video: With Hello 1.0.0 running, importing the 1.1.0 file with the + button shows Hello updated: 1.0.0 → 1.1.0. below the list, and the plugin panel reopens with its button now reading Insert v1.1.

Watch a folder

Desktop app (Windows)

From the File menu, choose Settings › Edit settings.json. In the file that opens, change the value of "plugins.devPath" to the absolute path of your plugin folder, and save. If the key isn’t there, add it. Write the path with /, like C:/Users/me/hello. With single backslashes, the file can’t be read, and your other settings go back to their defaults too. After you save, restart the app. From then on, each time you save a file in the folder, the plugin is registered again in every open editor, and a running plugin reopens. You don’t need to zip it. To turn this off, change the value to null and restart the app. The macOS app can’t read folders outside the app because of its sandbox, so on macOS, import again instead.

Plugins imported from a file are gone after a reload

An imported plugin lives only in the editor you have open now. When you reload or reopen the editor, it’s removed from the list, so import it again. Folder watching in the desktop app carries on even after you reopen the app.

Publish

A published plugin goes into the plugin list in Studio, where anyone can install it. A person reviews every version before it’s published.

  1. Open the developer console

    Open the developer console. You need to sign in to open it.

  2. Choose the package

    Click “Choose .sshowplugin” and choose your package. The server reads the manifest from the package, so there’s nothing else to fill in. Put anything you want to tell the reviewers in the note field.

  3. Request a review

    Check the consent box and click “Submit for review”. The plugin appears in the “My plugins” list below as “In review”.

  4. Check the result

    When the review is done, you get a notification and an email. An approved version is published to the list right away. If changes are requested, read “Reviewer feedback” in the “My plugins” list. Fix the plugin, and submit it again with a higher version.

Publishing rules
RuleDetails
idThe account that first submits it owns it from then on. Use an id that is a domain you manage, written in reverse. installed, mine, and submit can’t be used.
VersionEach submission needs a higher version than the last one. Raise the number even when you fix a rejected version and submit it again.
ReviewA plugin can have only one version in review at a time. Submit the next version after the result is in.
DeliveryThe reviewed zip file goes to users exactly as is. The server doesn’t change the package.
UnlistingClick “Unlist” in the “My plugins” list, and the plugin disappears from the plugin list in Studio. When a new version is approved, it’s published again.

When something goes wrong

  • Why do I get “This file could not be read as a .sshowplugin package.” when I import?

    It means the zip couldn’t be read or the manifest doesn’t follow the rules. Check that both files are at the top level of the zip, that plugin.json is valid JSON, and that id, version, and main follow the rules.
  • Why do I get “This plugin was built for a different plugin API version.”?

    api in plugin.json differs from the editor’s API version. An existing plugin with the same id is also removed from the list. Change api to 1, pack it again, and import it.
  • Why does nothing change when I call applyActions?

    The reason in the returned skipped tells you why each action was skipped. Common causes are the id of an object in another scene, an unknown set key, and a src that isn’t an asset:// URL. For an object in another scene, give sceneId as well.
  • Why does a rotation come out far too large?

    Angles you read from transform are in radians, but angles you send in set are read as degrees. When you send back a value you read, multiply it by 180 / Math.PI.
  • Why won’t script files, images, or fonts load?

    A plugin UI can’t use the network or outside files. Write scripts and styles directly in the UI document, and embed images and fonts as data: URLs.
  • Why don’t Space or Tab work inside my plugin?

    Both keys go on to the editor as shortcuts. To keep focus moves and button presses working, call stopPropagation() for those two keys in keydown and keyup listeners attached to document. Calling preventDefault() also keeps them from the editor, but stops the browser default too. Keys aren’t forwarded while an input field has focus.
  • Where do I see script errors?

    In the web editor, errors from the plugin frame also show in the console of the browser’s developer tools. In the desktop app, developer tools don’t open. If a plugin you changed through folder watching doesn’t appear in the list or doesn’t work, pack the same files, import them in the web editor, and check with the browser’s developer tools.
Was this page helpful?

Updated 10/12/2026 · The videos on this page were recorded in the current editor.

Popular